├── .github └── workflows │ └── build-and-publish.yaml ├── .gitignore ├── .gitmodules ├── Dockerfile ├── bot ├── blacklist │ └── cache.go ├── button │ ├── buttonresponse.go │ ├── handlers │ │ ├── addadmin.go │ │ ├── addsupport.go │ │ ├── claim.go │ │ ├── close.go │ │ ├── closeconfirm.go │ │ ├── closerequestaccept.go │ │ ├── closerequestdeny.go │ │ ├── closewithreasonmodal.go │ │ ├── closewithreasonsubmit.go │ │ ├── exitsurveysubmit.go │ │ ├── form.go │ │ ├── jointhread.go │ │ ├── languageselector.go │ │ ├── multipanel.go │ │ ├── opensurvey.go │ │ ├── panel.go │ │ ├── premiumcheckagain.go │ │ ├── premiumkeybutton.go │ │ ├── premiumkeysubmit.go │ │ ├── premiumselect.go │ │ ├── rate.go │ │ ├── redeemvotecredits.go │ │ ├── viewstaff.go │ │ └── viewsurvey.go │ ├── manager │ │ ├── handler.go │ │ ├── manager.go │ │ └── modal.go │ ├── registry │ │ ├── flag.go │ │ ├── handlers.go │ │ ├── matcher │ │ │ ├── default.go │ │ │ ├── func.go │ │ │ ├── matcher.go │ │ │ └── simple.go │ │ ├── properties.go │ │ └── registry.go │ ├── responseack.go │ ├── responseedit.go │ ├── responsemessage.go │ └── responsemodal.go ├── cache │ └── cacheclient.go ├── command │ ├── argument.go │ ├── commandcategory.go │ ├── context │ │ ├── applicationcommandcontext.go │ │ ├── autoclosecontext.go │ │ ├── buttoncontext.go │ │ ├── dashboardcontext.go │ │ ├── interactionextensions.go │ │ ├── mentionabletype.go │ │ ├── messagecomponentextensions.go │ │ ├── modalcontext.go │ │ ├── panelcontext.go │ │ ├── replyable.go │ │ ├── replycounter.go │ │ ├── selectmenucontext.go │ │ └── statecache.go │ ├── idretriever.go │ ├── impl │ │ ├── admin │ │ │ ├── admin.go │ │ │ ├── adminblacklist.go │ │ │ ├── admincheckblacklist.go │ │ │ ├── admincheckpremium.go │ │ │ ├── admingenpremium.go │ │ │ ├── admingetowner.go │ │ │ ├── adminlistguildentitlements.go │ │ │ ├── adminlistuserentitlements.go │ │ │ ├── adminrecache.go │ │ │ ├── adminwhitelabelassignguild.go │ │ │ └── adminwhitelabeldata.go │ │ ├── general │ │ │ ├── about.go │ │ │ ├── help.go │ │ │ ├── invite.go │ │ │ ├── jumptotop.go │ │ │ └── vote.go │ │ ├── settings │ │ │ ├── addadmin.go │ │ │ ├── addsupport.go │ │ │ ├── autoclose.go │ │ │ ├── autocloseconfigure.go │ │ │ ├── autocloseexclude.go │ │ │ ├── blacklist.go │ │ │ ├── language.go │ │ │ ├── panel.go │ │ │ ├── premium.go │ │ │ ├── removeadmin.go │ │ │ ├── removesupport.go │ │ │ ├── setup │ │ │ │ ├── auto.go │ │ │ │ ├── limit.go │ │ │ │ ├── setup.go │ │ │ │ ├── threads.go │ │ │ │ └── transcripts.go │ │ │ └── viewstaff.go │ │ ├── statistics │ │ │ ├── stats.go │ │ │ ├── statsserver.go │ │ │ └── statsuser.go │ │ ├── tags │ │ │ ├── managetags.go │ │ │ ├── managetagsadd.go │ │ │ ├── managetagsdelete.go │ │ │ ├── managetagslist.go │ │ │ ├── tag.go │ │ │ └── tagalias.go │ │ └── tickets │ │ │ ├── add.go │ │ │ ├── claim.go │ │ │ ├── close.go │ │ │ ├── closerequest.go │ │ │ ├── notes.go │ │ │ ├── oncall.go │ │ │ ├── open.go │ │ │ ├── remove.go │ │ │ ├── rename.go │ │ │ ├── reopen.go │ │ │ ├── startticket.go │ │ │ ├── switchpanel.go │ │ │ ├── transfer.go │ │ │ └── unclaim.go │ ├── manager │ │ └── manager.go │ ├── mentionabletype.go │ ├── messageresponse.go │ └── registry │ │ ├── command.go │ │ ├── commandcontext.go │ │ ├── properties.go │ │ ├── registry.go │ │ └── source.go ├── constants │ └── timeouts.go ├── customisation │ ├── colour.go │ └── emoji.go ├── dbclient │ ├── analytics.go │ ├── dbclient.go │ └── logger.go ├── errorcontext │ └── workererrorcontext.go ├── integrations │ ├── customintegrations.go │ ├── integrations.go │ └── secureproxy.go ├── listeners │ ├── channeldelete.go │ ├── guildcreate.go │ ├── guildleave.go │ ├── listeners.go │ ├── memberleave.go │ ├── memberupdate.go │ ├── message.go │ ├── messagequeue │ │ ├── autoclose.go │ │ ├── closerequesttimer.go │ │ ├── contextbuilder.go │ │ └── ticketclose.go │ ├── register.go │ ├── roledelete.go │ ├── threadmembersupdate.go │ └── threadupdate.go ├── logic │ ├── claim.go │ ├── close.go │ ├── closeembed.go │ ├── discordpermissions.go │ ├── open.go │ ├── permissions.go │ ├── reopen.go │ ├── substitutions.go │ ├── viewstaff.go │ └── welcomemessage.go ├── metrics │ ├── prometheus │ │ ├── hooks.go │ │ ├── prometheus.go │ │ └── server.go │ └── statsd │ │ ├── keys.go │ │ ├── resthook.go │ │ └── statsd.go ├── permissionwrapper │ └── permission.go ├── premium │ └── command.go ├── redis │ ├── channelrefetchtimer.go │ ├── client.go │ ├── commandids.go │ ├── dmchannelcache.go │ ├── integrationrolecache.go │ ├── openlock.go │ ├── renameratelimit.go │ ├── ticketchannelcache.go │ └── ticketratelimit.go ├── rpc │ └── listeners │ │ ├── base.go │ │ └── ticketstatusupdater.go └── utils │ ├── adminutils.go │ ├── archive.go │ ├── blacklist.go │ ├── context.go │ ├── conversion.go │ ├── discordutils.go │ ├── errorutils.go │ ├── hostname.go │ ├── interactionutils.go │ ├── markdown_test.go │ ├── messageutils.go │ ├── permissionutils.go │ ├── premium.go │ ├── proxyhook.go │ ├── retriever.go │ ├── sliceutils.go │ ├── stringutils.go │ └── timeutils.go ├── cmd ├── exportmessages │ └── main.go ├── registercommands │ └── main.go └── worker │ └── main.go ├── config └── config.go ├── context.go ├── event ├── caller.go ├── commandexecutor.go ├── errorcontext.go ├── eventexecutor.go ├── httplisten.go ├── json.go └── kafkalisten.go ├── go.mod ├── go.sum ├── i18n ├── get.go ├── locales.go └── messages.go ├── restwrapper.go └── tools └── cmd ├── cmdcaller.tmpl ├── generatecmdcaller.go ├── generatelisteners.go └── listeners.tmpl /.github/workflows/build-and-publish.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Publish 2 | 3 | env: 4 | REGISTRY: ghcr.io 5 | PACKAGE_NAME: worker 6 | GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} 7 | GITHUB_SHA: ${{ github.sha }} 8 | 9 | on: 10 | push: 11 | branches: [ "master", "sunset" ] 12 | workflow_dispatch: 13 | 14 | jobs: 15 | publish-image: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v3 24 | with: 25 | submodules: recursive 26 | 27 | - name: Print Go version 28 | run: | 29 | go version 30 | 31 | - name: Log in to the Container registry 32 | uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 33 | with: 34 | registry: ${{ env.REGISTRY }} 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Set image name 39 | run: | 40 | echo "IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY_OWNER,,}/${PACKAGE_NAME,,}:${GITHUB_SHA}" >> ${GITHUB_ENV} 41 | 42 | - name: Extract metadata (tags, labels) for Docker 43 | id: meta 44 | uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 45 | with: 46 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 47 | 48 | - name: Build and push Docker image 49 | uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 50 | with: 51 | context: . 52 | push: true 53 | tags: ${{ env.IMAGE_NAME }} 54 | labels: ${{ steps.meta.outputs.labels }} 55 | 56 | - name: Log image name 57 | run: | 58 | echo "Image URI: ${IMAGE_NAME}" 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .env -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "locale"] 2 | path = locale 3 | url = https://github.com/TicketsBot/locale.git 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build container 2 | FROM golang:1.22 AS builder 3 | 4 | RUN go version 5 | 6 | RUN apt-get update && apt-get upgrade -y && apt-get install -y ca-certificates git zlib1g-dev 7 | 8 | COPY . /go/src/github.com/TicketsBot/worker 9 | WORKDIR /go/src/github.com/TicketsBot/worker 10 | 11 | RUN git submodule update --init --recursive --remote 12 | 13 | RUN set -Eeux && \ 14 | go mod download && \ 15 | go mod verify 16 | 17 | RUN GOOS=linux GOARCH=amd64 \ 18 | go build \ 19 | -tags=jsoniter \ 20 | -trimpath \ 21 | -o main cmd/worker/main.go 22 | 23 | # Prod container 24 | FROM ubuntu:latest 25 | 26 | RUN apt-get update && apt-get upgrade -y && apt-get install -y ca-certificates curl 27 | 28 | COPY --from=builder /go/src/github.com/TicketsBot/worker/locale /srv/worker/locale 29 | COPY --from=builder /go/src/github.com/TicketsBot/worker/main /srv/worker/main 30 | 31 | RUN chmod +x /srv/worker/main 32 | 33 | RUN useradd -m container 34 | USER container 35 | WORKDIR /srv/worker 36 | 37 | CMD ["/srv/worker/main"] -------------------------------------------------------------------------------- /bot/blacklist/cache.go: -------------------------------------------------------------------------------- 1 | package blacklist 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/worker/bot/dbclient" 6 | "go.uber.org/zap" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | var ( 12 | blacklistedGuilds = make(map[uint64]struct{}) 13 | blacklistedUsers = make(map[uint64]struct{}) 14 | mu sync.RWMutex 15 | ) 16 | 17 | func IsGuildBlacklisted(guildId uint64) bool { 18 | mu.RLock() 19 | defer mu.RUnlock() 20 | 21 | _, ok := blacklistedGuilds[guildId] 22 | return ok 23 | } 24 | 25 | func IsUserBlacklisted(userId uint64) bool { 26 | mu.RLock() 27 | defer mu.RUnlock() 28 | 29 | _, ok := blacklistedUsers[userId] 30 | return ok 31 | } 32 | 33 | func RefreshCache(ctx context.Context) error { 34 | guildIds, err := dbclient.Client.ServerBlacklist.ListAll(ctx) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | userIds, err := dbclient.Client.GlobalBlacklist.ListAll(ctx) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | // Build new maps first instead of updating the existing ones to reduce lock time 45 | guildMap := sliceToMap(guildIds) 46 | userMap := sliceToMap(userIds) 47 | 48 | mu.Lock() 49 | defer mu.Unlock() 50 | 51 | blacklistedGuilds = guildMap 52 | blacklistedUsers = userMap 53 | 54 | return nil 55 | } 56 | 57 | func StartCacheRefreshLoop(logger *zap.Logger) { 58 | logger.Info("Starting blacklist cache refresh loop") 59 | 60 | timer := time.NewTicker(time.Minute * 5) 61 | 62 | for { 63 | <-timer.C 64 | 65 | if err := RefreshCache(context.Background()); err != nil { 66 | logger.Error("Failed to refresh blacklist cache", zap.Error(err)) 67 | continue 68 | } 69 | 70 | logger.Debug("Refreshed blacklist cache successfully") 71 | } 72 | } 73 | 74 | func sliceToMap(slice []uint64) map[uint64]struct{} { 75 | m := make(map[uint64]struct{}) 76 | for _, v := range slice { 77 | m[v] = struct{}{} 78 | } 79 | 80 | return m 81 | } 82 | -------------------------------------------------------------------------------- /bot/button/buttonresponse.go: -------------------------------------------------------------------------------- 1 | package button 2 | 3 | import ( 4 | "github.com/TicketsBot/worker" 5 | "github.com/rxdn/gdl/objects/interaction" 6 | ) 7 | 8 | type Response interface { 9 | Type() ResponseType 10 | Build() interface{} // Returns the interaction response struct 11 | HandleDeferred(interactionData interaction.InteractionMetadata, worker *worker.Context) error 12 | } 13 | 14 | type ResponseType uint8 15 | 16 | const ( 17 | ResponseTypeMessage ResponseType = iota 18 | ResponseTypeEdit 19 | ResponseTypeModal 20 | ResponseTypeAck 21 | ) 22 | -------------------------------------------------------------------------------- /bot/button/handlers/claim.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/button/registry" 7 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 8 | "github.com/TicketsBot/worker/bot/command" 9 | "github.com/TicketsBot/worker/bot/command/context" 10 | "github.com/TicketsBot/worker/bot/constants" 11 | "github.com/TicketsBot/worker/bot/customisation" 12 | "github.com/TicketsBot/worker/bot/dbclient" 13 | "github.com/TicketsBot/worker/bot/logic" 14 | "github.com/TicketsBot/worker/i18n" 15 | "github.com/rxdn/gdl/objects/interaction/component" 16 | ) 17 | 18 | type ClaimHandler struct{} 19 | 20 | func (h *ClaimHandler) Matcher() matcher.Matcher { 21 | return &matcher.SimpleMatcher{ 22 | CustomId: "claim", 23 | } 24 | } 25 | 26 | func (h *ClaimHandler) Properties() registry.Properties { 27 | return registry.Properties{ 28 | Flags: registry.SumFlags(registry.GuildAllowed, registry.CanEdit), 29 | Timeout: constants.TimeoutOpenTicket, 30 | } 31 | } 32 | 33 | func (h *ClaimHandler) Execute(ctx *context.ButtonContext) { 34 | // Get permission level 35 | permissionLevel, err := ctx.UserPermissionLevel(ctx) 36 | if err != nil { 37 | ctx.HandleError(err) 38 | return 39 | } 40 | 41 | if permissionLevel < permission.Support { 42 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageClaimNoPermission) 43 | return 44 | } 45 | 46 | // Get ticket struct 47 | ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, ctx.ChannelId(), ctx.GuildId()) 48 | if err != nil { 49 | ctx.HandleError(err) 50 | return 51 | } 52 | 53 | // Verify this is a ticket channel 54 | if ticket.UserId == 0 { 55 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageNotATicketChannel) 56 | return 57 | } 58 | 59 | if err := logic.ClaimTicket(ctx.Context, ctx, ticket, ctx.UserId()); err != nil { 60 | ctx.HandleError(err) 61 | return 62 | } 63 | 64 | res := command.MessageIntoMessageResponse(ctx.Interaction.Message) 65 | if len(res.Components) > 0 && res.Components[0].Type == component.ComponentActionRow { 66 | row := res.Components[0].ComponentData.(component.ActionRow) 67 | if len(row.Components) > 1 { 68 | row.Components = row.Components[:len(row.Components)-1] 69 | } 70 | 71 | res.Components[0] = component.Component{ 72 | Type: component.ComponentActionRow, 73 | ComponentData: row, 74 | } 75 | } 76 | 77 | ctx.Edit(res) 78 | ctx.ReplyPermanent(customisation.Green, i18n.TitleClaimed, i18n.MessageClaimed, fmt.Sprintf("<@%d>", ctx.UserId())) 79 | } 80 | -------------------------------------------------------------------------------- /bot/button/handlers/close.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/TicketsBot/worker/bot/button/registry" 5 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 6 | "github.com/TicketsBot/worker/bot/command" 7 | cmdcontext "github.com/TicketsBot/worker/bot/command/context" 8 | "github.com/TicketsBot/worker/bot/constants" 9 | "github.com/TicketsBot/worker/bot/customisation" 10 | "github.com/TicketsBot/worker/bot/dbclient" 11 | "github.com/TicketsBot/worker/bot/logic" 12 | "github.com/TicketsBot/worker/bot/utils" 13 | "github.com/TicketsBot/worker/i18n" 14 | "github.com/rxdn/gdl/objects/channel/embed" 15 | "github.com/rxdn/gdl/objects/interaction/component" 16 | ) 17 | 18 | type CloseHandler struct{} 19 | 20 | func (h *CloseHandler) Matcher() matcher.Matcher { 21 | return &matcher.SimpleMatcher{ 22 | CustomId: "close", 23 | } 24 | } 25 | 26 | func (h *CloseHandler) Properties() registry.Properties { 27 | return registry.Properties{ 28 | Flags: registry.SumFlags(registry.GuildAllowed), 29 | Timeout: constants.TimeoutCloseTicket, 30 | } 31 | } 32 | 33 | func (h *CloseHandler) Execute(ctx *cmdcontext.ButtonContext) { 34 | // Get the ticket properties 35 | ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, ctx.ChannelId(), ctx.GuildId()) 36 | if err != nil { 37 | ctx.HandleError(err) 38 | return 39 | } 40 | 41 | // Check that this channel is a ticket channel 42 | if ticket.GuildId == 0 { 43 | return 44 | } 45 | 46 | // This is checked by the close function, but we need to check before showing close confirmation 47 | if !utils.CanClose(ctx, ctx, ticket) { 48 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageCloseNoPermission) 49 | return 50 | } 51 | 52 | closeConfirmation, err := dbclient.Client.CloseConfirmation.Get(ctx, ctx.GuildId()) 53 | if err != nil { 54 | ctx.HandleError(err) 55 | return 56 | } 57 | 58 | if closeConfirmation { 59 | // Send confirmation message 60 | confirmEmbed := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleCloseConfirmation, i18n.MessageCloseConfirmation, nil) 61 | confirmEmbed.SetAuthor(ctx.InteractionUser().Username, "", utils.Ptr(ctx.InteractionUser()).AvatarUrl(256)) 62 | 63 | msgData := command.MessageResponse{ 64 | Embeds: []*embed.Embed{confirmEmbed}, 65 | Components: []component.Component{ 66 | component.BuildActionRow(component.BuildButton(component.Button{ 67 | Label: ctx.GetMessage(i18n.TitleClose), 68 | CustomId: "close_confirm", 69 | Style: component.ButtonStylePrimary, 70 | Emoji: utils.BuildEmoji("✔️"), 71 | })), 72 | }, 73 | } 74 | 75 | if _, err := ctx.ReplyWith(msgData); err != nil { 76 | ctx.HandleError(err) 77 | return 78 | } 79 | } else { 80 | // TODO: IntoPanelContext()? 81 | logic.CloseTicket(ctx.Context, ctx, nil, false) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /bot/button/handlers/closeconfirm.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/TicketsBot/worker/bot/button/registry" 5 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 6 | "github.com/TicketsBot/worker/bot/command/context" 7 | "github.com/TicketsBot/worker/bot/constants" 8 | "github.com/TicketsBot/worker/bot/logic" 9 | ) 10 | 11 | type CloseConfirmHandler struct{} 12 | 13 | func (h *CloseConfirmHandler) Matcher() matcher.Matcher { 14 | return &matcher.SimpleMatcher{ 15 | CustomId: "close_confirm", 16 | } 17 | } 18 | 19 | func (h *CloseConfirmHandler) Properties() registry.Properties { 20 | return registry.Properties{ 21 | Flags: registry.SumFlags(registry.GuildAllowed), 22 | Timeout: constants.TimeoutCloseTicket, 23 | } 24 | } 25 | 26 | func (h *CloseConfirmHandler) Execute(ctx *context.ButtonContext) { 27 | // TODO: IntoPanelContext()? 28 | logic.CloseTicket(ctx.Context, ctx, nil, false) 29 | } 30 | -------------------------------------------------------------------------------- /bot/button/handlers/closerequestaccept.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/TicketsBot/common/premium" 5 | "github.com/TicketsBot/worker/bot/button/registry" 6 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 7 | "github.com/TicketsBot/worker/bot/command" 8 | "github.com/TicketsBot/worker/bot/command/context" 9 | "github.com/TicketsBot/worker/bot/constants" 10 | "github.com/TicketsBot/worker/bot/customisation" 11 | "github.com/TicketsBot/worker/bot/dbclient" 12 | "github.com/TicketsBot/worker/bot/logic" 13 | "github.com/TicketsBot/worker/bot/utils" 14 | "github.com/TicketsBot/worker/i18n" 15 | ) 16 | 17 | type CloseRequestAcceptHandler struct{} 18 | 19 | func (h *CloseRequestAcceptHandler) Matcher() matcher.Matcher { 20 | return &matcher.SimpleMatcher{ 21 | CustomId: "close_request_accept", 22 | } 23 | } 24 | 25 | func (h *CloseRequestAcceptHandler) Properties() registry.Properties { 26 | return registry.Properties{ 27 | Flags: registry.SumFlags(registry.GuildAllowed), 28 | Timeout: constants.TimeoutCloseTicket, 29 | } 30 | } 31 | 32 | func (h *CloseRequestAcceptHandler) Execute(ctx *context.ButtonContext) { 33 | ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, ctx.ChannelId(), ctx.GuildId()) 34 | if err != nil { 35 | ctx.HandleError(err) 36 | return 37 | } 38 | 39 | if ticket.Id == 0 { 40 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageNotATicketChannel) 41 | return 42 | } 43 | 44 | if ctx.UserId() != ticket.UserId { 45 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageCloseRequestNoPermission) 46 | return 47 | } 48 | 49 | closeRequest, ok, err := dbclient.Client.CloseRequest.Get(ctx, ticket.GuildId, ticket.Id) 50 | if err != nil { 51 | ctx.HandleError(err) 52 | return 53 | } 54 | 55 | // Infallible, unless malicious 56 | if !ok { 57 | return 58 | } 59 | 60 | ctx.Edit(command.MessageResponse{ 61 | Embeds: utils.Slice(utils.BuildEmbedRaw(customisation.DefaultColours[customisation.Green], "Close Request", "Closing ticket...", nil, premium.Whitelabel)), // TODO: Translations, calculate premium level 62 | }) 63 | 64 | // Avoid users cant close issue 65 | // Allow members to close too, for context menu tickets 66 | logic.CloseTicket(ctx.Context, ctx, closeRequest.Reason, true) 67 | } 68 | -------------------------------------------------------------------------------- /bot/button/handlers/closerequestdeny.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/TicketsBot/worker/bot/button/registry" 5 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/context" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/bot/utils" 11 | "github.com/TicketsBot/worker/i18n" 12 | "time" 13 | ) 14 | 15 | type CloseRequestDenyHandler struct{} 16 | 17 | func (h *CloseRequestDenyHandler) Matcher() matcher.Matcher { 18 | return &matcher.SimpleMatcher{ 19 | CustomId: "close_request_deny", 20 | } 21 | } 22 | 23 | func (h *CloseRequestDenyHandler) Properties() registry.Properties { 24 | return registry.Properties{ 25 | Flags: registry.SumFlags(registry.GuildAllowed), 26 | Timeout: time.Second * 3, 27 | } 28 | } 29 | 30 | func (h *CloseRequestDenyHandler) Execute(ctx *context.ButtonContext) { 31 | ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, ctx.ChannelId(), ctx.GuildId()) 32 | if err != nil { 33 | ctx.HandleError(err) 34 | return 35 | } 36 | 37 | if ticket.Id == 0 { 38 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageNotATicketChannel) 39 | return 40 | } 41 | 42 | if ctx.UserId() != ticket.UserId { 43 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageCloseRequestNoPermission) 44 | return 45 | } 46 | 47 | if err := dbclient.Client.CloseRequest.Delete(ctx, ctx.GuildId(), ticket.Id); err != nil { 48 | ctx.HandleError(err) 49 | return 50 | } 51 | 52 | ctx.Edit(command.MessageResponse{ 53 | Embeds: utils.Embeds(utils.BuildEmbed(ctx, customisation.Red, i18n.TitleCloseRequest, i18n.MessageCloseRequestDenied, nil, ctx.UserId())), 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /bot/button/handlers/closewithreasonmodal.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/TicketsBot/worker/bot/button" 5 | "github.com/TicketsBot/worker/bot/button/registry" 6 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 7 | "github.com/TicketsBot/worker/bot/command/context" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/bot/utils" 11 | "github.com/TicketsBot/worker/i18n" 12 | "github.com/rxdn/gdl/objects/interaction" 13 | "github.com/rxdn/gdl/objects/interaction/component" 14 | "time" 15 | ) 16 | 17 | type CloseWithReasonModalHandler struct{} 18 | 19 | func (h *CloseWithReasonModalHandler) Matcher() matcher.Matcher { 20 | return &matcher.SimpleMatcher{ 21 | CustomId: "close_with_reason", 22 | } 23 | } 24 | 25 | func (h *CloseWithReasonModalHandler) Properties() registry.Properties { 26 | return registry.Properties{ 27 | Flags: registry.SumFlags(registry.GuildAllowed), 28 | Timeout: time.Second * 3, 29 | } 30 | } 31 | 32 | func (h *CloseWithReasonModalHandler) Execute(ctx *context.ButtonContext) { 33 | ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, ctx.ChannelId(), ctx.GuildId()) 34 | if err != nil { 35 | ctx.HandleError(err) 36 | return 37 | } 38 | 39 | if ticket.Id == 0 { 40 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageNotATicketChannel) 41 | return 42 | } 43 | 44 | if !utils.CanClose(ctx.Context, ctx, ticket) { 45 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageCloseNoPermission) 46 | return 47 | } 48 | 49 | ctx.Modal(button.ResponseModal{ 50 | Data: interaction.ModalResponseData{ 51 | CustomId: "close_with_reason_submit", 52 | Title: i18n.TitleClose.GetFromGuild(ctx.GuildId()), 53 | Components: []component.Component{ 54 | component.BuildActionRow(component.BuildInputText(component.InputText{ 55 | Style: component.TextStyleParagraph, 56 | CustomId: "reason", 57 | Label: i18n.Reason.GetFromGuild(ctx.GuildId()), 58 | Placeholder: utils.Ptr(i18n.MessageCloseReasonPlaceholder.GetFromGuild(ctx.GuildId())), 59 | MinLength: nil, 60 | MaxLength: utils.Ptr(uint32(1024)), 61 | })), 62 | }, 63 | }, 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /bot/button/handlers/closewithreasonsubmit.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/worker/bot/button/registry" 6 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 7 | "github.com/TicketsBot/worker/bot/command/context" 8 | "github.com/TicketsBot/worker/bot/constants" 9 | "github.com/TicketsBot/worker/bot/logic" 10 | ) 11 | 12 | type CloseWithReasonSubmitHandler struct{} 13 | 14 | func (h *CloseWithReasonSubmitHandler) Matcher() matcher.Matcher { 15 | return matcher.NewSimpleMatcher("close_with_reason_submit") 16 | } 17 | 18 | func (h *CloseWithReasonSubmitHandler) Properties() registry.Properties { 19 | return registry.Properties{ 20 | Flags: registry.SumFlags(registry.GuildAllowed), 21 | Timeout: constants.TimeoutCloseTicket, 22 | } 23 | } 24 | 25 | func (h *CloseWithReasonSubmitHandler) Execute(ctx *context.ModalContext) { 26 | data := ctx.Interaction.Data 27 | 28 | // Get the reason 29 | if len(data.Components) == 0 { // No action rows 30 | ctx.HandleError(fmt.Errorf("No action rows found in modal components")) 31 | return 32 | } 33 | 34 | actionRow := data.Components[0] 35 | if len(actionRow.Components) == 0 { // Text input missing 36 | ctx.HandleError(fmt.Errorf("Modal missing text input")) 37 | return 38 | } 39 | 40 | textInput := actionRow.Components[0] 41 | if textInput.CustomId != "reason" { 42 | ctx.HandleError(fmt.Errorf("Text input custom ID mismatch")) 43 | return 44 | } 45 | 46 | // This must be malicious 47 | if len(textInput.Value) > 1024 { 48 | ctx.HandleError(fmt.Errorf("Reason is too long")) 49 | return 50 | } 51 | 52 | ctx.Ack() 53 | logic.CloseTicket(ctx.Context, ctx, &textInput.Value, false) 54 | } 55 | -------------------------------------------------------------------------------- /bot/button/handlers/form.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/TicketsBot/common/sentry" 5 | "github.com/TicketsBot/database" 6 | "github.com/TicketsBot/worker/bot/button/registry" 7 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 8 | "github.com/TicketsBot/worker/bot/command/context" 9 | "github.com/TicketsBot/worker/bot/constants" 10 | "github.com/TicketsBot/worker/bot/customisation" 11 | "github.com/TicketsBot/worker/bot/dbclient" 12 | "github.com/TicketsBot/worker/bot/logic" 13 | "github.com/TicketsBot/worker/i18n" 14 | "strings" 15 | ) 16 | 17 | type FormHandler struct{} 18 | 19 | func (h *FormHandler) Matcher() matcher.Matcher { 20 | return matcher.NewFuncMatcher(func(customId string) bool { 21 | return strings.HasPrefix(customId, "form_") 22 | }) 23 | } 24 | 25 | func (h *FormHandler) Properties() registry.Properties { 26 | return registry.Properties{ 27 | Flags: registry.SumFlags(registry.GuildAllowed), 28 | Timeout: constants.TimeoutOpenTicket, 29 | } 30 | } 31 | 32 | func (h *FormHandler) Execute(ctx *context.ModalContext) { 33 | data := ctx.Interaction.Data 34 | customId := strings.TrimPrefix(data.CustomId, "form_") // get the custom id that is used in the database 35 | 36 | // Form IDs aren't unique to a panel, so we submit the modal with a custom id of `form_panelcustomid` 37 | panel, ok, err := dbclient.Client.Panel.GetByCustomId(ctx, ctx.GuildId(), customId) 38 | if err != nil { 39 | sentry.Error(err) // TODO: Proper context 40 | return 41 | } 42 | 43 | if ok { 44 | // TODO: Log this 45 | if panel.GuildId != ctx.GuildId() { 46 | return 47 | } 48 | 49 | // blacklist check 50 | blacklisted, err := ctx.IsBlacklisted(ctx) 51 | if err != nil { 52 | ctx.HandleError(err) 53 | return 54 | } 55 | 56 | if blacklisted { 57 | ctx.Reply(customisation.Red, i18n.TitleBlacklisted, i18n.MessageBlacklisted) 58 | return 59 | } 60 | 61 | inputs, err := dbclient.Client.FormInput.GetAllInputsByCustomId(ctx, ctx.GuildId()) 62 | if err != nil { 63 | ctx.HandleError(err) 64 | return 65 | } 66 | 67 | formAnswers := make(map[database.FormInput]string) 68 | for _, actionRow := range data.Components { 69 | for _, input := range actionRow.Components { 70 | questionData, ok := inputs[input.CustomId] 71 | if ok { // If form has changed, we can skip 72 | formAnswers[questionData] = input.Value 73 | } 74 | } 75 | } 76 | 77 | // Validate user input 78 | for question, answer := range formAnswers { 79 | if !question.Required { 80 | continue 81 | } 82 | 83 | // Check that users have not just pressed newline or space 84 | isValid := false 85 | for _, c := range answer { 86 | if c != rune(' ') && c != rune('\n') { 87 | isValid = true 88 | break 89 | } 90 | } 91 | 92 | if !isValid { 93 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageFormMissingInput, question.Label) 94 | return 95 | } 96 | } 97 | 98 | ctx.Defer() 99 | _, _ = logic.OpenTicket(ctx.Context, ctx, &panel, panel.Title, formAnswers) 100 | 101 | return 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /bot/button/handlers/jointhread.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "errors" 5 | "github.com/TicketsBot/worker/bot/button/registry" 6 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 7 | "github.com/TicketsBot/worker/bot/command/context" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/bot/logic" 11 | "github.com/TicketsBot/worker/i18n" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | type JoinThreadHandler struct{} 19 | 20 | func (h *JoinThreadHandler) Matcher() matcher.Matcher { 21 | return &matcher.FuncMatcher{ 22 | Func: func(customId string) bool { 23 | return strings.HasPrefix(customId, "join_thread_") 24 | }, 25 | } 26 | } 27 | 28 | func (h *JoinThreadHandler) Properties() registry.Properties { 29 | return registry.Properties{ 30 | Flags: registry.SumFlags(registry.GuildAllowed), 31 | Timeout: time.Second * 5, 32 | } 33 | } 34 | 35 | var joinThreadPattern = regexp.MustCompile(`join_thread_(\d+)`) 36 | 37 | func (h *JoinThreadHandler) Execute(ctx *context.ButtonContext) { 38 | groups := joinThreadPattern.FindStringSubmatch(ctx.InteractionData.CustomId) 39 | if len(groups) < 2 { 40 | return 41 | } 42 | 43 | // Errors are impossible 44 | ticketId, _ := strconv.Atoi(groups[1]) 45 | 46 | // Get ticket 47 | ticket, err := dbclient.Client.Tickets.Get(ctx, ticketId, ctx.GuildId()) 48 | if err != nil { 49 | ctx.HandleError(err) 50 | return 51 | } 52 | 53 | if !ticket.IsThread { 54 | ctx.HandleError(errors.New("Ticket is not a thread")) 55 | return 56 | } 57 | 58 | if !ticket.Open { 59 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageJoinClosedTicket) 60 | 61 | // Try to delete message 62 | _ = ctx.Worker().DeleteMessage(ctx.ChannelId(), ctx.Interaction.Message.Id) 63 | 64 | return 65 | } 66 | 67 | if ticket.ChannelId == nil { 68 | ctx.HandleError(errors.New("Ticket channel not found")) 69 | return 70 | } 71 | 72 | // Check permission 73 | hasPermission, err := logic.HasPermissionForTicket(ctx, ctx.Worker(), ticket, ctx.UserId()) 74 | if err != nil { 75 | ctx.HandleError(err) 76 | return 77 | } 78 | 79 | if !hasPermission { 80 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageJoinThreadNoPermission) 81 | return 82 | } 83 | 84 | if _, err := ctx.Worker().GetThreadMember(*ticket.ChannelId, ctx.UserId()); err == nil { 85 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageAlreadyJoinedThread, *ticket.ChannelId) 86 | return 87 | } 88 | 89 | // Join ticket 90 | if err := ctx.Worker().AddThreadMember(*ticket.ChannelId, ctx.UserId()); err != nil { 91 | ctx.HandleError(err) 92 | return 93 | } 94 | 95 | ctx.Reply(customisation.Green, i18n.Success, i18n.MessageJoinThreadSuccess, *ticket.ChannelId) 96 | } 97 | -------------------------------------------------------------------------------- /bot/button/handlers/languageselector.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/button/registry" 6 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 7 | "github.com/TicketsBot/worker/bot/command/context" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/i18n" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type LanguageSelectorHandler struct{} 16 | 17 | func (h *LanguageSelectorHandler) Matcher() matcher.Matcher { 18 | return matcher.NewFuncMatcher(func(customId string) bool { 19 | return strings.HasPrefix(customId, "language-selector-") 20 | }) 21 | } 22 | 23 | func (h *LanguageSelectorHandler) Properties() registry.Properties { 24 | return registry.Properties{ 25 | Flags: registry.SumFlags(registry.GuildAllowed), 26 | Timeout: time.Second * 3, 27 | } 28 | } 29 | 30 | func (h *LanguageSelectorHandler) Execute(ctx *context.SelectMenuContext) { 31 | permissionLevel, err := ctx.UserPermissionLevel(ctx) 32 | if err != nil { 33 | ctx.HandleError(err) 34 | return 35 | } 36 | 37 | if permissionLevel < permission.Admin { 38 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageNoPermission) 39 | return 40 | } 41 | 42 | if len(ctx.InteractionData.Values) == 0 { 43 | return 44 | } 45 | 46 | newLocale, ok := i18n.MappedByIsoShortCode[ctx.InteractionData.Values[0]] 47 | // Infallible 48 | if !ok { 49 | ctx.ReplyRaw(customisation.Red, "Error", "Invalid language") 50 | return 51 | } 52 | 53 | if err := dbclient.Client.ActiveLanguage.Set(ctx, ctx.GuildId(), newLocale.IsoShortCode); err != nil { 54 | ctx.HandleError(err) 55 | return 56 | } 57 | 58 | ctx.Reply(customisation.Green, i18n.TitleLanguage, i18n.MessageLanguageSuccess, newLocale.LocalName, newLocale.FlagEmoji) 59 | } 60 | -------------------------------------------------------------------------------- /bot/button/handlers/multipanel.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "errors" 5 | "github.com/TicketsBot/common/sentry" 6 | "github.com/TicketsBot/worker/bot/button/registry" 7 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 8 | "github.com/TicketsBot/worker/bot/command/context" 9 | "github.com/TicketsBot/worker/bot/constants" 10 | "github.com/TicketsBot/worker/bot/customisation" 11 | "github.com/TicketsBot/worker/bot/dbclient" 12 | "github.com/TicketsBot/worker/bot/logic" 13 | "github.com/TicketsBot/worker/i18n" 14 | ) 15 | 16 | type MultiPanelHandler struct{} 17 | 18 | func (h *MultiPanelHandler) Matcher() matcher.Matcher { 19 | return &matcher.SimpleMatcher{ 20 | CustomId: "multipanel", 21 | } 22 | } 23 | 24 | func (h *MultiPanelHandler) Properties() registry.Properties { 25 | return registry.Properties{ 26 | Flags: registry.SumFlags(registry.GuildAllowed), 27 | Timeout: constants.TimeoutOpenTicket, 28 | } 29 | } 30 | 31 | func (h *MultiPanelHandler) Execute(ctx *context.SelectMenuContext) { 32 | if len(ctx.InteractionData.Values) == 0 { 33 | return 34 | } 35 | 36 | panelCustomId := ctx.InteractionData.Values[0] 37 | 38 | panel, ok, err := dbclient.Client.Panel.GetByCustomId(ctx, ctx.GuildId(), panelCustomId) 39 | if err != nil { 40 | sentry.Error(err) // TODO: Proper context 41 | return 42 | } 43 | 44 | if ok { 45 | // TODO: Log this 46 | if panel.GuildId != ctx.GuildId() { 47 | return 48 | } 49 | 50 | // blacklist check 51 | blacklisted, err := ctx.IsBlacklisted(ctx) 52 | if err != nil { 53 | ctx.HandleError(err) 54 | return 55 | } 56 | 57 | if blacklisted { 58 | ctx.Reply(customisation.Red, i18n.TitleBlacklisted, i18n.MessageBlacklisted) 59 | return 60 | } 61 | 62 | if panel.FormId == nil { 63 | _, _ = logic.OpenTicket(ctx.Context, ctx, &panel, panel.Title, nil) 64 | } else { 65 | form, ok, err := dbclient.Client.Forms.Get(ctx, *panel.FormId) 66 | if err != nil { 67 | ctx.HandleError(err) 68 | return 69 | } 70 | 71 | if !ok { 72 | ctx.HandleError(errors.New("Form not found")) 73 | return 74 | } 75 | 76 | inputs, err := dbclient.Client.FormInput.GetInputs(ctx, form.Id) 77 | if err != nil { 78 | ctx.HandleError(err) 79 | return 80 | } 81 | 82 | if len(inputs) == 0 { // Don't open a blank form 83 | _, _ = logic.OpenTicket(ctx.Context, ctx, &panel, panel.Title, nil) 84 | } else { 85 | modal := buildForm(panel, form, inputs) 86 | ctx.Modal(modal) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /bot/button/handlers/premiumcheckagain.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/common/premium" 6 | "github.com/TicketsBot/worker/bot/button/registry" 7 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 8 | "github.com/TicketsBot/worker/bot/command/context" 9 | "github.com/TicketsBot/worker/bot/customisation" 10 | "github.com/TicketsBot/worker/bot/dbclient" 11 | prem "github.com/TicketsBot/worker/bot/premium" 12 | "github.com/TicketsBot/worker/bot/utils" 13 | "github.com/TicketsBot/worker/i18n" 14 | "time" 15 | ) 16 | 17 | type PremiumCheckAgain struct{} 18 | 19 | func (h *PremiumCheckAgain) Matcher() matcher.Matcher { 20 | return &matcher.SimpleMatcher{ 21 | CustomId: "premium_check_again", 22 | } 23 | } 24 | 25 | func (h *PremiumCheckAgain) Properties() registry.Properties { 26 | return registry.Properties{ 27 | Flags: registry.SumFlags(registry.GuildAllowed), 28 | Timeout: time.Second * 5, 29 | } 30 | } 31 | 32 | func (h *PremiumCheckAgain) Execute(ctx *context.ButtonContext) { 33 | // Get permission level 34 | permissionLevel, err := ctx.UserPermissionLevel(ctx) 35 | if err != nil { 36 | ctx.HandleError(err) 37 | return 38 | } 39 | 40 | if permissionLevel < permission.Admin { 41 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageNoPermission) 42 | return 43 | } 44 | 45 | ctx.EditWith(customisation.Green, i18n.MessagePremiumChecking, i18n.MessagePremiumPleaseWait) 46 | 47 | if err := utils.PremiumClient.DeleteCachedTier(ctx, ctx.GuildId()); err != nil { 48 | ctx.HandleError(err) 49 | return 50 | } 51 | 52 | if ctx.PremiumTier() > premium.None { 53 | ctx.EditWith(customisation.Green, i18n.Success, i18n.MessagePremiumSuccessAfterCheck) 54 | 55 | // Re-enable panels 56 | if err := dbclient.Client.Panel.EnableAll(ctx, ctx.GuildId()); err != nil { 57 | ctx.HandleError(err) 58 | return 59 | } 60 | } else { 61 | entitlement, err := dbclient.Client.LegacyPremiumEntitlements.GetUserTier(ctx, ctx.UserId(), premium.PatreonGracePeriod) 62 | if err != nil { 63 | ctx.HandleError(err) 64 | return 65 | } 66 | 67 | if entitlement == nil { 68 | ctx.Edit(prem.BuildPatreonNotLinkedMessage(ctx)) 69 | } else { 70 | res, err := prem.BuildPatreonSubscriptionFoundMessage(ctx, entitlement) 71 | if err != nil { 72 | ctx.HandleError(err) 73 | return 74 | } 75 | 76 | ctx.Edit(res) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /bot/button/handlers/premiumkeybutton.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/button" 6 | "github.com/TicketsBot/worker/bot/button/registry" 7 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 8 | "github.com/TicketsBot/worker/bot/command/context" 9 | "github.com/TicketsBot/worker/bot/customisation" 10 | prem "github.com/TicketsBot/worker/bot/premium" 11 | "github.com/TicketsBot/worker/i18n" 12 | "time" 13 | ) 14 | 15 | type PremiumKeyButtonHandler struct{} 16 | 17 | func (h *PremiumKeyButtonHandler) Matcher() matcher.Matcher { 18 | return &matcher.SimpleMatcher{ 19 | CustomId: "open_premium_key_modal", 20 | } 21 | } 22 | 23 | func (h *PremiumKeyButtonHandler) Properties() registry.Properties { 24 | return registry.Properties{ 25 | Flags: registry.SumFlags(registry.GuildAllowed), 26 | Timeout: time.Second * 3, 27 | } 28 | } 29 | 30 | func (h *PremiumKeyButtonHandler) Execute(ctx *context.ButtonContext) { 31 | // Get permission level 32 | permissionLevel, err := ctx.UserPermissionLevel(ctx) 33 | if err != nil { 34 | ctx.HandleError(err) 35 | return 36 | } 37 | 38 | if permissionLevel < permission.Admin { 39 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageNoPermission) 40 | return 41 | } 42 | 43 | ctx.Modal(button.ResponseModal{ 44 | Data: prem.BuildKeyModal(ctx.GuildId()), 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /bot/button/handlers/premiumselect.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/common/premium" 7 | "github.com/TicketsBot/worker/bot/button" 8 | "github.com/TicketsBot/worker/bot/button/registry" 9 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 10 | "github.com/TicketsBot/worker/bot/command/context" 11 | "github.com/TicketsBot/worker/bot/customisation" 12 | "github.com/TicketsBot/worker/bot/dbclient" 13 | prem "github.com/TicketsBot/worker/bot/premium" 14 | "github.com/TicketsBot/worker/bot/utils" 15 | "github.com/TicketsBot/worker/i18n" 16 | "github.com/rxdn/gdl/objects/interaction/component" 17 | "time" 18 | ) 19 | 20 | type PremiumKeyOpenHandler struct{} 21 | 22 | func (h *PremiumKeyOpenHandler) Matcher() matcher.Matcher { 23 | return matcher.NewSimpleMatcher("premium_purchase_method") 24 | } 25 | 26 | func (h *PremiumKeyOpenHandler) Properties() registry.Properties { 27 | return registry.Properties{ 28 | Flags: registry.SumFlags(registry.GuildAllowed, registry.CanEdit), 29 | Timeout: time.Second * 5, 30 | } 31 | } 32 | 33 | func (h *PremiumKeyOpenHandler) Execute(ctx *context.SelectMenuContext) { 34 | permLevel, err := ctx.UserPermissionLevel(ctx) 35 | if err != nil { 36 | ctx.HandleError(err) 37 | return 38 | } 39 | 40 | if permLevel < permission.Admin { 41 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageNoPermission) 42 | return 43 | } 44 | 45 | if len(ctx.InteractionData.Values) == 0 { 46 | return 47 | } 48 | 49 | option := ctx.InteractionData.Values[0] 50 | if option == "patreon" { 51 | entitlement, err := dbclient.Client.LegacyPremiumEntitlements.GetUserTier(ctx, ctx.UserId(), premium.PatreonGracePeriod) 52 | if err != nil { 53 | ctx.HandleError(err) 54 | return 55 | } 56 | 57 | if entitlement == nil { 58 | ctx.Edit(prem.BuildPatreonNotLinkedMessage(ctx)) 59 | } else { 60 | res, err := prem.BuildPatreonSubscriptionFoundMessage(ctx, entitlement) 61 | if err != nil { 62 | ctx.HandleError(err) 63 | return 64 | } 65 | 66 | ctx.Edit(res) 67 | } 68 | } else if option == "discord" { 69 | ctx.Edit(prem.BuildDiscordNotFoundMessage(ctx)) 70 | } else if option == "key" { 71 | ctx.Modal(button.ResponseModal{ 72 | Data: prem.BuildKeyModal(ctx.GuildId()), 73 | }) 74 | 75 | components := utils.Slice(component.BuildActionRow(component.BuildButton(component.Button{ 76 | Label: ctx.GetMessage(i18n.MessagePremiumOpenForm), 77 | CustomId: "open_premium_key_modal", 78 | Style: component.ButtonStylePrimary, 79 | Emoji: utils.BuildEmoji("🔑"), 80 | }))) 81 | 82 | ctx.EditWithComponents(customisation.Green, i18n.TitlePremium, i18n.MessagePremiumOpenFormDescription, components) 83 | } else { 84 | ctx.HandleError(fmt.Errorf("Invalid premium purchase method: %s", option)) 85 | return 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /bot/button/handlers/redeemvotecredits.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "errors" 5 | "github.com/TicketsBot/common/model" 6 | "github.com/TicketsBot/common/permission" 7 | "github.com/TicketsBot/worker/bot/button/registry" 8 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 9 | "github.com/TicketsBot/worker/bot/command/context" 10 | "github.com/TicketsBot/worker/bot/customisation" 11 | "github.com/TicketsBot/worker/bot/dbclient" 12 | "github.com/TicketsBot/worker/bot/utils" 13 | "github.com/TicketsBot/worker/config" 14 | "github.com/TicketsBot/worker/i18n" 15 | "github.com/jackc/pgx/v4" 16 | "github.com/rxdn/gdl/objects/interaction/component" 17 | "time" 18 | ) 19 | 20 | type RedeemVoteCreditsHandler struct{} 21 | 22 | func (h *RedeemVoteCreditsHandler) Matcher() matcher.Matcher { 23 | return &matcher.SimpleMatcher{ 24 | CustomId: "redeem_vote_credits", 25 | } 26 | } 27 | 28 | func (h *RedeemVoteCreditsHandler) Properties() registry.Properties { 29 | return registry.Properties{ 30 | Flags: registry.SumFlags(registry.GuildAllowed, registry.CanEdit), 31 | PermissionLevel: permission.Support, 32 | Timeout: time.Second * 5, 33 | } 34 | } 35 | 36 | var errNoCredits = errors.New("no credits") 37 | 38 | func (h *RedeemVoteCreditsHandler) Execute(ctx *context.ButtonContext) { 39 | var credits int 40 | if err := dbclient.Client.WithTx(ctx, func(tx pgx.Tx) error { 41 | var err error 42 | credits, err = dbclient.Client.VoteCredits.Get(ctx, tx, ctx.UserId()) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | if credits <= 0 { 48 | ctx.EditWithComponents(customisation.Red, i18n.Error, i18n.MessageVoteNoCredits, make([]component.Component, 0)) 49 | return errNoCredits 50 | } 51 | 52 | if err := dbclient.Client.VoteCredits.Delete(ctx, tx, ctx.UserId()); err != nil { 53 | return err 54 | } 55 | 56 | if err := dbclient.Client.Entitlements.IncreaseExpiry( 57 | ctx, 58 | tx, 59 | utils.Ptr(ctx.GuildId()), 60 | utils.Ptr(ctx.UserId()), 61 | config.Conf.VoteSkuId, 62 | model.EntitlementSourceVoting, 63 | time.Hour*24*time.Duration(credits), 64 | ); err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | }); err != nil { 70 | if errors.Is(err, errNoCredits) { 71 | return 72 | } 73 | 74 | ctx.HandleError(err) 75 | return 76 | } 77 | 78 | // TODO: dbclient.Client.Panels.EnableAll? 79 | 80 | if err := utils.PremiumClient.DeleteCachedTier(ctx, ctx.GuildId()); err != nil { 81 | ctx.HandleError(err) 82 | return 83 | } 84 | 85 | if credits == 1 { 86 | ctx.EditWithComponents(customisation.Green, i18n.Success, i18n.MessageVoteRedeemSuccessSingular, make([]component.Component, 0), credits) 87 | } else { 88 | ctx.EditWithComponents(customisation.Green, i18n.Success, i18n.MessageVoteRedeemSuccessPlural, make([]component.Component, 0), credits) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /bot/button/handlers/viewstaff.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/worker/bot/button/registry" 6 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 7 | "github.com/TicketsBot/worker/bot/command" 8 | "github.com/TicketsBot/worker/bot/command/context" 9 | "github.com/TicketsBot/worker/bot/logic" 10 | "github.com/rxdn/gdl/objects/channel/embed" 11 | "github.com/rxdn/gdl/objects/guild/emoji" 12 | "github.com/rxdn/gdl/objects/interaction/component" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | type ViewStaffHandler struct{} 20 | 21 | func (h *ViewStaffHandler) Matcher() matcher.Matcher { 22 | return &matcher.FuncMatcher{ 23 | Func: func(customId string) bool { 24 | return strings.HasPrefix(customId, "viewstaff_") 25 | }, 26 | } 27 | } 28 | 29 | func (h *ViewStaffHandler) Properties() registry.Properties { 30 | return registry.Properties{ 31 | Flags: registry.SumFlags(registry.GuildAllowed, registry.CanEdit), 32 | Timeout: time.Second * 5, 33 | } 34 | } 35 | 36 | var viewStaffPattern = regexp.MustCompile(`viewstaff_(\d+)`) 37 | 38 | func (h *ViewStaffHandler) Execute(ctx *context.ButtonContext) { 39 | groups := viewStaffPattern.FindStringSubmatch(ctx.InteractionData.CustomId) 40 | if len(groups) < 2 { 41 | return 42 | } 43 | 44 | page, err := strconv.Atoi(groups[1]) 45 | if err != nil { 46 | return 47 | } 48 | 49 | if page < 0 { 50 | return 51 | } 52 | 53 | msgEmbed, isBlank := logic.BuildViewStaffMessage(ctx.Context, ctx, page) 54 | if !isBlank { 55 | ctx.Edit(command.MessageResponse{ 56 | Embeds: []*embed.Embed{msgEmbed}, 57 | Components: []component.Component{ 58 | component.BuildActionRow( 59 | component.BuildButton(component.Button{ 60 | CustomId: fmt.Sprintf("viewstaff_%d", page-1), 61 | Style: component.ButtonStylePrimary, 62 | Emoji: &emoji.Emoji{ 63 | Name: "◀️", 64 | }, 65 | Disabled: page <= 0, 66 | }), 67 | component.BuildButton(component.Button{ 68 | CustomId: fmt.Sprintf("viewstaff_%d", page+1), 69 | Style: component.ButtonStylePrimary, 70 | Emoji: &emoji.Emoji{ 71 | Name: "▶️", 72 | }, 73 | Disabled: false, 74 | }), 75 | ), 76 | }, 77 | }) 78 | } else { 79 | components := ctx.Interaction.Message.Components 80 | if len(components) == 0 { // Impossible unless whitelabel 81 | return 82 | } 83 | 84 | actionRow, ok := components[0].ComponentData.(component.ActionRow) 85 | if !ok { 86 | return 87 | } 88 | 89 | if len(actionRow.Components) < 2 { 90 | return 91 | } 92 | 93 | nextButton := actionRow.Components[1].ComponentData.(component.Button) 94 | if !ok { 95 | return 96 | } 97 | 98 | nextButton.Disabled = true 99 | actionRow.Components[1].ComponentData = nextButton 100 | components[0].ComponentData = actionRow 101 | 102 | // v hacky 103 | embeds := make([]*embed.Embed, len(ctx.Interaction.Message.Embeds)) 104 | for i, e := range ctx.Interaction.Message.Embeds { 105 | embeds[i] = &e 106 | } 107 | 108 | ctx.Edit(command.MessageResponse{ 109 | Embeds: embeds, 110 | Components: components, 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /bot/button/manager/modal.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/premium" 6 | "github.com/TicketsBot/common/sentry" 7 | "github.com/TicketsBot/worker" 8 | "github.com/TicketsBot/worker/bot/button" 9 | cmdcontext "github.com/TicketsBot/worker/bot/command/context" 10 | "github.com/TicketsBot/worker/bot/errorcontext" 11 | "github.com/TicketsBot/worker/config" 12 | "github.com/rxdn/gdl/objects/interaction" 13 | "time" 14 | ) 15 | 16 | func HandleModalInteraction(ctx context.Context, manager *ComponentInteractionManager, worker *worker.Context, data interaction.ModalSubmitInteraction, responseCh chan button.Response) bool { 17 | // Safety checks 18 | if data.GuildId.Value != 0 && data.Member == nil { 19 | return false 20 | } 21 | 22 | if data.GuildId.Value == 0 && data.User == nil { 23 | return false 24 | } 25 | 26 | lookupCtx, cancelLookupCtx := context.WithTimeout(ctx, time.Second*2) 27 | defer cancelLookupCtx() 28 | 29 | premiumTier, err := getPremiumTier(lookupCtx, worker, data.GuildId.Value) 30 | if err != nil { 31 | sentry.ErrorWithContext(err, errorcontext.WorkerErrorContext{ 32 | Guild: data.GuildId.Value, 33 | Channel: data.ChannelId, 34 | }) 35 | 36 | premiumTier = premium.None 37 | } 38 | 39 | if premiumTier == premium.None && config.Conf.PremiumOnly { 40 | return false 41 | } 42 | 43 | handler := manager.MatchModal(data.Data.CustomId) 44 | if handler == nil { 45 | return false 46 | } 47 | 48 | ctx, cancel := context.WithTimeout(ctx, handler.Properties().Timeout) 49 | 50 | cc := cmdcontext.NewModalContext(ctx, worker, data, premiumTier, responseCh) 51 | shouldExecute, canEdit := doPropertiesChecks(lookupCtx, data.GuildId.Value, cc, handler.Properties()) 52 | if shouldExecute { 53 | go func() { 54 | defer cancel() 55 | handler.Execute(cc) 56 | }() 57 | } else { 58 | cancel() 59 | } 60 | 61 | return canEdit 62 | } 63 | -------------------------------------------------------------------------------- /bot/button/registry/flag.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | type Flag int 4 | 5 | const ( 6 | DMsAllowed Flag = iota 7 | GuildAllowed 8 | CanEdit 9 | ) 10 | 11 | func (f Flag) Int() int { 12 | return int(f) 13 | } 14 | -------------------------------------------------------------------------------- /bot/button/registry/handlers.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "github.com/TicketsBot/worker/bot/button/registry/matcher" 5 | "github.com/TicketsBot/worker/bot/command/context" 6 | ) 7 | 8 | type ButtonHandler interface { 9 | Matcher() matcher.Matcher 10 | Properties() Properties 11 | Execute(ctx *context.ButtonContext) 12 | } 13 | 14 | type SelectHandler interface { 15 | Matcher() matcher.Matcher 16 | Properties() Properties 17 | Execute(ctx *context.SelectMenuContext) 18 | } 19 | 20 | type ModalHandler interface { 21 | Matcher() matcher.Matcher 22 | Properties() Properties 23 | Execute(ctx *context.ModalContext) 24 | } 25 | -------------------------------------------------------------------------------- /bot/button/registry/matcher/default.go: -------------------------------------------------------------------------------- 1 | package matcher 2 | 3 | type DefaultMatcher struct { 4 | } 5 | 6 | func NewDefaultMatcher(prefix string) *DefaultMatcher { 7 | return &DefaultMatcher{} 8 | } 9 | 10 | func (m *DefaultMatcher) Type() Type { 11 | return TypeDefault 12 | } 13 | -------------------------------------------------------------------------------- /bot/button/registry/matcher/func.go: -------------------------------------------------------------------------------- 1 | package matcher 2 | 3 | type FuncMatchEngine func(customId string) bool 4 | 5 | type FuncMatcher struct { 6 | Func FuncMatchEngine 7 | } 8 | 9 | func NewFuncMatcher(engine FuncMatchEngine) *FuncMatcher { 10 | return &FuncMatcher{ 11 | Func: engine, 12 | } 13 | } 14 | 15 | func (m *FuncMatcher) Type() Type { 16 | return TypeFunc 17 | } 18 | -------------------------------------------------------------------------------- /bot/button/registry/matcher/matcher.go: -------------------------------------------------------------------------------- 1 | package matcher 2 | 3 | type Matcher interface { 4 | Type() Type 5 | } 6 | 7 | type Type uint8 8 | 9 | const ( 10 | TypeSimple Type = iota 11 | TypeFunc 12 | TypeDefault 13 | ) 14 | 15 | 16 | -------------------------------------------------------------------------------- /bot/button/registry/matcher/simple.go: -------------------------------------------------------------------------------- 1 | package matcher 2 | 3 | type SimpleMatcher struct { 4 | CustomId string 5 | } 6 | 7 | func NewSimpleMatcher(customId string) *SimpleMatcher { 8 | return &SimpleMatcher{ 9 | CustomId: customId, 10 | } 11 | } 12 | 13 | func (m *SimpleMatcher) Type() Type { 14 | return TypeSimple 15 | } 16 | -------------------------------------------------------------------------------- /bot/button/registry/properties.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "time" 6 | ) 7 | 8 | type Properties struct { 9 | Flags int 10 | PermissionLevel permission.PermissionLevel 11 | Timeout time.Duration 12 | } 13 | 14 | func (p *Properties) HasFlag(flag Flag) bool { 15 | return p.Flags&flag.Int() == flag.Int() 16 | } 17 | 18 | func SumFlags(flags ...Flag) (sum int) { 19 | for _, flag := range flags { 20 | sum |= flag.Int() 21 | } 22 | 23 | return sum 24 | } 25 | -------------------------------------------------------------------------------- /bot/button/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | type ( 4 | ButtonRegistry []ButtonHandler 5 | SelectRegistry []SelectHandler 6 | ModalRegistry []ModalHandler 7 | ) 8 | -------------------------------------------------------------------------------- /bot/button/responseack.go: -------------------------------------------------------------------------------- 1 | package button 2 | 3 | import ( 4 | "github.com/TicketsBot/worker" 5 | "github.com/rxdn/gdl/objects/interaction" 6 | ) 7 | 8 | type ResponseAck struct{} 9 | 10 | func (r ResponseAck) Type() ResponseType { 11 | return ResponseTypeAck 12 | } 13 | 14 | func (r ResponseAck) Build() interface{} { 15 | return interaction.NewResponseDeferredMessageUpdate() 16 | } 17 | 18 | func (r ResponseAck) HandleDeferred(interactionData interaction.InteractionMetadata, worker *worker.Context) error { 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /bot/button/responseedit.go: -------------------------------------------------------------------------------- 1 | package button 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/worker" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/rxdn/gdl/objects/interaction" 8 | "github.com/rxdn/gdl/rest" 9 | ) 10 | 11 | type ResponseEdit struct { 12 | Data command.MessageResponse 13 | } 14 | 15 | func (r ResponseEdit) Type() ResponseType { 16 | return ResponseTypeEdit 17 | } 18 | 19 | func (r ResponseEdit) Build() interface{} { 20 | return interaction.NewResponseUpdateMessage(r.Data.IntoUpdateMessageResponse()) 21 | } 22 | 23 | func (r ResponseEdit) HandleDeferred(interactionData interaction.InteractionMetadata, worker *worker.Context) error { 24 | _, err := rest.EditOriginalInteractionResponse(context.Background(), interactionData.Token, worker.RateLimiter, interactionData.ApplicationId, r.Data.IntoWebhookEditBody()) 25 | return err 26 | } 27 | -------------------------------------------------------------------------------- /bot/button/responsemessage.go: -------------------------------------------------------------------------------- 1 | package button 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/worker" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/utils" 8 | "github.com/rxdn/gdl/objects/interaction" 9 | "github.com/rxdn/gdl/rest" 10 | "time" 11 | ) 12 | 13 | type ResponseMessage struct { 14 | Data command.MessageResponse 15 | } 16 | 17 | func (r ResponseMessage) Type() ResponseType { 18 | return ResponseTypeMessage 19 | } 20 | 21 | func (r ResponseMessage) Build() interface{} { 22 | return interaction.NewResponseChannelMessage(r.Data.IntoApplicationCommandData()) 23 | } 24 | 25 | func (r ResponseMessage) HandleDeferred(interactionData interaction.InteractionMetadata, worker *worker.Context) error { 26 | if time.Now().Sub(utils.SnowflakeToTime(interactionData.Id)) > time.Minute*14 { 27 | return nil 28 | } 29 | 30 | _, err := rest.CreateFollowupMessage(context.Background(), interactionData.Token, worker.RateLimiter, worker.BotId, r.Data.IntoWebhookBody()) 31 | return err 32 | } 33 | -------------------------------------------------------------------------------- /bot/button/responsemodal.go: -------------------------------------------------------------------------------- 1 | package button 2 | 3 | import ( 4 | "errors" 5 | "github.com/TicketsBot/worker" 6 | "github.com/rxdn/gdl/objects/interaction" 7 | ) 8 | 9 | type ResponseModal struct { 10 | Data interaction.ModalResponseData 11 | } 12 | 13 | func (r ResponseModal) Type() ResponseType { 14 | return ResponseTypeModal 15 | } 16 | 17 | func (r ResponseModal) Build() interface{} { 18 | return interaction.NewModalResponse(r.Data.CustomId, r.Data.Title, r.Data.Components) 19 | } 20 | 21 | func (r ResponseModal) HandleDeferred(interactionData interaction.InteractionMetadata, worker *worker.Context) error { 22 | return errors.New("cannot defer modal response") 23 | } 24 | -------------------------------------------------------------------------------- /bot/cache/cacheclient.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/TicketsBot/worker/bot/dbclient" 7 | "github.com/TicketsBot/worker/config" 8 | "github.com/jackc/pgx/v4" 9 | "github.com/jackc/pgx/v4/pgxpool" 10 | "github.com/rxdn/gdl/cache" 11 | "go.uber.org/zap" 12 | "time" 13 | ) 14 | 15 | var Client *cache.PgCache 16 | 17 | func Connect(logger *zap.Logger) (client cache.PgCache, err error) { 18 | uri := fmt.Sprintf( 19 | "postgres://%s:%s@%s/%s?pool_max_conns=%d", 20 | config.Conf.Cache.Username, 21 | config.Conf.Cache.Password, 22 | config.Conf.Cache.Host, 23 | config.Conf.Cache.Database, 24 | config.Conf.Cache.Threads, 25 | ) 26 | 27 | cfg, err := pgxpool.ParseConfig(uri) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | logger.Info( 33 | "Connecting to Postgres cache", 34 | zap.String("username", config.Conf.Cache.Username), 35 | zap.String("host", config.Conf.Cache.Host), 36 | zap.String("database", config.Conf.Cache.Database), 37 | zap.Int("threads", config.Conf.Cache.Threads), 38 | ) 39 | 40 | // TODO: Sentry 41 | cfg.ConnConfig.LogLevel = pgx.LogLevelWarn 42 | cfg.ConnConfig.Logger = dbclient.NewLogAdapter(logger) 43 | cfg.ConnConfig.PreferSimpleProtocol = true 44 | cfg.ConnConfig.ConnectTimeout = time.Second * 15 45 | 46 | ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*30) 47 | defer cancelFunc() 48 | 49 | pool, err := pgxpool.ConnectConfig(ctx, cfg) 50 | if err != nil { 51 | return 52 | } 53 | 54 | client = cache.NewPgCache(pool, cache.CacheOptions{ 55 | Guilds: true, 56 | Users: true, 57 | Members: true, 58 | Channels: true, 59 | Roles: false, 60 | Emojis: false, 61 | VoiceStates: false, 62 | }) 63 | 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /bot/command/argument.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/TicketsBot/worker/i18n" 5 | "github.com/rxdn/gdl/objects/interaction" 6 | ) 7 | 8 | type Argument struct { 9 | Name string 10 | Description string 11 | Type interaction.ApplicationCommandOptionType 12 | Required bool 13 | InvalidMessage i18n.MessageId 14 | AutoCompleteHandler AutoCompleteHandler 15 | } 16 | 17 | type AutoCompleteHandler func(data interaction.ApplicationCommandAutoCompleteInteraction, value string) []interaction.ApplicationCommandOptionChoice 18 | 19 | func NewOptionalArgument(name, description string, argumentType interaction.ApplicationCommandOptionType, invalidMessage i18n.MessageId) Argument { 20 | return Argument{ 21 | Name: name, 22 | Description: description, 23 | Type: argumentType, 24 | Required: false, 25 | InvalidMessage: invalidMessage, 26 | AutoCompleteHandler: nil, 27 | } 28 | } 29 | 30 | func NewRequiredArgument(name, description string, argumentType interaction.ApplicationCommandOptionType, invalidMessage i18n.MessageId) Argument { 31 | return Argument{ 32 | Name: name, 33 | Description: description, 34 | Type: argumentType, 35 | Required: true, 36 | InvalidMessage: invalidMessage, 37 | AutoCompleteHandler: nil, 38 | } 39 | } 40 | 41 | func NewOptionalAutocompleteableArgument(name, description string, argumentType interaction.ApplicationCommandOptionType, invalidMessage i18n.MessageId, autoCompleteHandler AutoCompleteHandler) Argument { 42 | return Argument{ 43 | Name: name, 44 | Description: description, 45 | Type: argumentType, 46 | Required: false, 47 | InvalidMessage: invalidMessage, 48 | AutoCompleteHandler: autoCompleteHandler, 49 | } 50 | } 51 | 52 | func NewRequiredAutocompleteableArgument(name, description string, argumentType interaction.ApplicationCommandOptionType, invalidMessage i18n.MessageId, autoCompleteHandler AutoCompleteHandler) Argument { 53 | return Argument{ 54 | Name: name, 55 | Description: description, 56 | Type: argumentType, 57 | Required: true, 58 | InvalidMessage: invalidMessage, 59 | AutoCompleteHandler: autoCompleteHandler, 60 | } 61 | } 62 | 63 | func Arguments(argument ...Argument) []Argument { 64 | return argument 65 | } 66 | -------------------------------------------------------------------------------- /bot/command/commandcategory.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | type Category string 4 | 5 | const ( 6 | General Category = "ℹ️ General" 7 | Tickets Category = "📩 Tickets" 8 | Settings Category = "🔧 Settings" 9 | Tags Category = "✍️ Tags" 10 | Statistics Category = "📈 Statistics" 11 | ) 12 | 13 | var Categories = []Category{ 14 | General, 15 | Tickets, 16 | Settings, 17 | Tags, 18 | Statistics, 19 | } 20 | -------------------------------------------------------------------------------- /bot/command/context/interactionextensions.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/rxdn/gdl/objects" 5 | "github.com/rxdn/gdl/objects/channel" 6 | "github.com/rxdn/gdl/objects/channel/message" 7 | "github.com/rxdn/gdl/objects/guild" 8 | "github.com/rxdn/gdl/objects/interaction" 9 | "github.com/rxdn/gdl/objects/member" 10 | "github.com/rxdn/gdl/objects/user" 11 | ) 12 | 13 | type InteractionExtension struct { 14 | interaction interaction.ApplicationCommandInteraction 15 | } 16 | 17 | func NewInteractionExtension(interaction interaction.ApplicationCommandInteraction) InteractionExtension { 18 | return InteractionExtension{ 19 | interaction: interaction, 20 | } 21 | } 22 | 23 | func (i InteractionExtension) Resolved() interaction.ResolvedData { 24 | return i.interaction.Data.Resolved 25 | } 26 | 27 | func (i InteractionExtension) ResolvedUser(id uint64) (user.User, bool) { 28 | user, ok := i.interaction.Data.Resolved.Users[objects.Snowflake(id)] 29 | return user, ok 30 | } 31 | 32 | func (i InteractionExtension) ResolvedMember(id uint64) (member.Member, bool) { 33 | member, ok := i.interaction.Data.Resolved.Members[objects.Snowflake(id)] 34 | return member, ok 35 | } 36 | 37 | func (i InteractionExtension) ResolvedRole(id uint64) (guild.Role, bool) { 38 | role, ok := i.interaction.Data.Resolved.Roles[objects.Snowflake(id)] 39 | return role, ok 40 | } 41 | 42 | func (i InteractionExtension) ResolvedChannel(id uint64) (channel.Channel, bool) { 43 | channel, ok := i.interaction.Data.Resolved.Channels[objects.Snowflake(id)] 44 | return channel, ok 45 | } 46 | 47 | func (i InteractionExtension) ResolvedMessage(id uint64) (message.Message, bool) { 48 | message, ok := i.interaction.Data.Resolved.Messages[objects.Snowflake(id)] 49 | return message, ok 50 | } 51 | 52 | func (i InteractionExtension) ResolvedAttachment(id uint64) (channel.Attachment, bool) { 53 | attachment, ok := i.interaction.Data.Resolved.Attachments[objects.Snowflake(id)] 54 | return attachment, ok 55 | } 56 | -------------------------------------------------------------------------------- /bot/command/context/mentionabletype.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/TicketsBot/worker/bot/command/registry" 5 | "github.com/rxdn/gdl/objects" 6 | "github.com/rxdn/gdl/objects/channel" 7 | ) 8 | 9 | type MentionableType uint8 10 | 11 | const ( 12 | MentionableTypeUser MentionableType = iota 13 | MentionableTypeRole 14 | ) 15 | 16 | func (m MentionableType) OverwriteType() channel.PermissionOverwriteType { 17 | switch m { 18 | case MentionableTypeUser: 19 | return channel.PermissionTypeMember 20 | case MentionableTypeRole: 21 | return channel.PermissionTypeRole 22 | default: 23 | return -1 24 | } 25 | } 26 | 27 | // DetermineMentionableType TODO: Move this function to be a method on the CommandContext interface 28 | // DetermineMentionableType (type, ok) 29 | func DetermineMentionableType(ctx registry.CommandContext, id uint64) (MentionableType, bool) { 30 | interactionCtx, ok := ctx.(*SlashCommandContext) 31 | if ok { 32 | resolved := interactionCtx.Interaction.Data.Resolved 33 | if _, isUser := resolved.Users[objects.Snowflake(id)]; isUser { 34 | return MentionableTypeUser, true 35 | } else if _, isRole := resolved.Roles[objects.Snowflake(id)]; isRole { 36 | return MentionableTypeRole, true 37 | } else { 38 | return 0, false 39 | } 40 | } else { 41 | return 0, false 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /bot/command/context/replycounter.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "errors" 5 | "sync/atomic" 6 | ) 7 | 8 | const InteractionReplyLimit = 5 9 | 10 | type ReplyCounter struct { 11 | count atomic.Int32 12 | } 13 | 14 | var ErrReplyLimitReached = errors.New("reply limit reached") 15 | 16 | func NewReplyCounter() *ReplyCounter { 17 | return &ReplyCounter{} 18 | } 19 | 20 | func (r *ReplyCounter) Increment() int { 21 | return int(r.count.Add(1)) 22 | } 23 | 24 | func (r *ReplyCounter) Try() error { 25 | if r.count.Add(1) > InteractionReplyLimit { 26 | return ErrReplyLimitReached 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /bot/command/context/statecache.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/database" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/bot/dbclient" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type StateCache struct { 13 | ctx registry.CommandContext 14 | 15 | settings *database.Settings 16 | settingsMu sync.Mutex 17 | } 18 | 19 | func NewStateCache(ctx registry.CommandContext) *StateCache { 20 | return &StateCache{ 21 | ctx: ctx, 22 | } 23 | } 24 | 25 | func (s *StateCache) Settings() (database.Settings, error) { 26 | s.settingsMu.Lock() 27 | defer s.settingsMu.Unlock() 28 | 29 | if s.settings != nil { 30 | return *s.settings, nil 31 | } 32 | 33 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) 34 | defer cancel() 35 | 36 | settings, err := dbclient.Client.Settings.Get(ctx, s.ctx.GuildId()) 37 | if err != nil { 38 | return database.Settings{}, err 39 | } 40 | 41 | s.settings = &settings 42 | return settings, nil 43 | } 44 | -------------------------------------------------------------------------------- /bot/command/idretriever.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/TicketsBot/worker" 5 | "github.com/TicketsBot/worker/bot/redis" 6 | ) 7 | 8 | func LoadCommandIds(worker *worker.Context, botId uint64) (map[string]uint64, error) { 9 | // Check cache first 10 | cached, err := redis.LoadCommandIds(botId) 11 | if err == nil && len(cached) > 0 { 12 | return cached, nil 13 | } 14 | 15 | if err != nil && err != redis.ErrNil { 16 | return nil, err 17 | } 18 | 19 | // Not cached 20 | commands, err := worker.GetGlobalCommands(botId) // TODO: Do we store guild commands? 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | mapped := make(map[string]uint64) 26 | for _, command := range commands { 27 | mapped[command.Name] = command.Id 28 | } 29 | 30 | if err := redis.StoreCommandIds(botId, mapped); err != nil { 31 | return nil, err 32 | } 33 | 34 | return mapped, nil 35 | } 36 | -------------------------------------------------------------------------------- /bot/command/impl/admin/admin.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/i18n" 8 | "github.com/rxdn/gdl/objects/interaction" 9 | ) 10 | 11 | type AdminCommand struct { 12 | } 13 | 14 | func (AdminCommand) Properties() registry.Properties { 15 | return registry.Properties{ 16 | Name: "admin", 17 | Description: i18n.HelpAdmin, 18 | Type: interaction.ApplicationCommandTypeChatInput, 19 | Aliases: []string{"a"}, 20 | PermissionLevel: permission.Everyone, 21 | Children: []registry.Command{ 22 | AdminBlacklistCommand{}, 23 | AdminCheckBlacklistCommand{}, 24 | AdminCheckPremiumCommand{}, 25 | AdminGenPremiumCommand{}, 26 | AdminGetOwnerCommand{}, 27 | AdminListGuildEntitlementsCommand{}, 28 | AdminListUserEntitlementsCommand{}, 29 | AdminRecacheCommand{}, 30 | AdminWhitelabelAssignGuildCommand{}, 31 | AdminWhitelabelDataCommand{}, 32 | }, 33 | Category: command.Settings, 34 | HelperOnly: true, 35 | } 36 | } 37 | 38 | func (c AdminCommand) GetExecutor() interface{} { 39 | return c.Execute 40 | } 41 | 42 | func (AdminCommand) Execute(_ registry.CommandContext) { 43 | // Cannot execute parent command directly 44 | } 45 | -------------------------------------------------------------------------------- /bot/command/impl/admin/adminblacklist.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/i18n" 11 | "github.com/rxdn/gdl/objects/interaction" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | type AdminBlacklistCommand struct { 17 | } 18 | 19 | func (AdminBlacklistCommand) Properties() registry.Properties { 20 | return registry.Properties{ 21 | Name: "blacklist", 22 | Description: i18n.HelpAdminBlacklist, 23 | Type: interaction.ApplicationCommandTypeChatInput, 24 | PermissionLevel: permission.Everyone, 25 | Category: command.Settings, 26 | AdminOnly: true, 27 | Arguments: command.Arguments( 28 | command.NewRequiredArgument("guild_id", "ID of the guild to blacklist", interaction.OptionTypeString, i18n.MessageInvalidArgument), 29 | command.NewOptionalArgument("reason", "Reason for blacklisting the guild", interaction.OptionTypeString, i18n.MessageInvalidArgument), 30 | ), 31 | Timeout: time.Second * 10, 32 | } 33 | } 34 | 35 | func (c AdminBlacklistCommand) GetExecutor() interface{} { 36 | return c.Execute 37 | } 38 | 39 | func (AdminBlacklistCommand) Execute(ctx registry.CommandContext, raw string, reason *string) { 40 | guildId, err := strconv.ParseUint(raw, 10, 64) 41 | if err != nil { 42 | ctx.ReplyRaw(customisation.Red, ctx.GetMessage(i18n.Error), "Invalid guild ID provided") 43 | return 44 | } 45 | 46 | if err := dbclient.Client.ServerBlacklist.Add(ctx, guildId, reason); err != nil { 47 | ctx.HandleError(err) 48 | return 49 | } 50 | 51 | ctx.ReplyPlainPermanent("🔨") 52 | 53 | // Check if whitelabel 54 | botId, ok, err := dbclient.Client.WhitelabelGuilds.GetBotByGuild(ctx, guildId) 55 | if err != nil { 56 | ctx.HandleError(err) 57 | return 58 | } 59 | 60 | var w *worker.Context 61 | if ok { // Whitelabel bot 62 | // Get bot 63 | bot, err := dbclient.Client.Whitelabel.GetByBotId(ctx, botId) 64 | if err != nil { 65 | ctx.HandleError(err) 66 | return 67 | } 68 | 69 | w = &worker.Context{ 70 | Token: bot.Token, 71 | BotId: bot.BotId, 72 | IsWhitelabel: true, 73 | Cache: ctx.Worker().Cache, 74 | RateLimiter: nil, // Use http-proxy ratelimit functionality 75 | } 76 | } else { // Public bot 77 | w = ctx.Worker() 78 | } 79 | 80 | if err := w.LeaveGuild(guildId); err != nil { 81 | ctx.HandleError(err) 82 | return 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /bot/command/impl/admin/admincheckblacklist.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/bot/utils" 11 | "github.com/TicketsBot/worker/i18n" 12 | "github.com/rxdn/gdl/objects/interaction" 13 | "strconv" 14 | "time" 15 | ) 16 | 17 | type AdminCheckBlacklistCommand struct { 18 | } 19 | 20 | func (AdminCheckBlacklistCommand) Properties() registry.Properties { 21 | return registry.Properties{ 22 | Name: "check-blacklist", 23 | Description: i18n.HelpAdmin, 24 | Type: interaction.ApplicationCommandTypeChatInput, 25 | PermissionLevel: permission.Everyone, 26 | Category: command.Settings, 27 | HelperOnly: true, 28 | Arguments: command.Arguments( 29 | command.NewRequiredArgument("guild_id", "ID of the guild to unblacklist", interaction.OptionTypeString, i18n.MessageInvalidArgument), 30 | ), 31 | Timeout: time.Second * 10, 32 | } 33 | } 34 | 35 | func (c AdminCheckBlacklistCommand) GetExecutor() interface{} { 36 | return c.Execute 37 | } 38 | 39 | func (AdminCheckBlacklistCommand) Execute(ctx registry.CommandContext, raw string) { 40 | guildId, err := strconv.ParseUint(raw, 10, 64) 41 | if err != nil { 42 | ctx.ReplyRaw(customisation.Red, ctx.GetMessage(i18n.Error), "Invalid guild ID provided") 43 | return 44 | } 45 | 46 | isBlacklisted, reason, err := dbclient.Client.ServerBlacklist.IsBlacklisted(ctx, guildId) 47 | if err != nil { 48 | ctx.HandleError(err) 49 | return 50 | } 51 | 52 | if isBlacklisted { 53 | reasonFormatted := utils.ValueOrDefault(reason, "No reason provided") 54 | ctx.ReplyRaw(customisation.Orange, "Blacklist Check", fmt.Sprintf("This guild is blacklisted.\n```%s```", reasonFormatted)) 55 | } else { 56 | ctx.ReplyRaw(customisation.Green, "Blacklist Check", "This guild is not blacklisted") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /bot/command/impl/admin/admincheckpremium.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/utils" 10 | "github.com/TicketsBot/worker/i18n" 11 | "github.com/rxdn/gdl/objects/interaction" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | type AdminCheckPremiumCommand struct { 17 | } 18 | 19 | func (AdminCheckPremiumCommand) Properties() registry.Properties { 20 | return registry.Properties{ 21 | Name: "checkpremium", 22 | Description: i18n.HelpAdminCheckPremium, 23 | Type: interaction.ApplicationCommandTypeChatInput, 24 | PermissionLevel: permission.Everyone, 25 | Category: command.Settings, 26 | HelperOnly: true, 27 | Arguments: command.Arguments( 28 | command.NewRequiredArgument("guild_id", "ID of the guild to check premium status for", interaction.OptionTypeString, i18n.MessageInvalidArgument), 29 | ), 30 | Timeout: time.Second * 10, 31 | } 32 | } 33 | 34 | func (c AdminCheckPremiumCommand) GetExecutor() interface{} { 35 | return c.Execute 36 | } 37 | 38 | func (AdminCheckPremiumCommand) Execute(ctx registry.CommandContext, raw string) { 39 | guildId, err := strconv.ParseUint(raw, 10, 64) 40 | if err != nil { 41 | ctx.ReplyRaw(customisation.Red, ctx.GetMessage(i18n.Error), "Invalid guild ID provided") 42 | return 43 | } 44 | 45 | guild, err := ctx.Worker().GetGuild(guildId) 46 | if err != nil { 47 | ctx.ReplyRaw(customisation.Red, ctx.GetMessage(i18n.Error), err.Error()) 48 | return 49 | } 50 | 51 | tier, src, err := utils.PremiumClient.GetTierByGuild(ctx, guild) 52 | if err != nil { 53 | ctx.HandleError(err) 54 | return 55 | } 56 | 57 | ctx.ReplyRaw(customisation.Green, ctx.GetMessage(i18n.Admin), fmt.Sprintf("`%s` (owner <@%d> %d) has premium tier %d (src %s)", guild.Name, guild.OwnerId, guild.OwnerId, tier, src)) 58 | } 59 | -------------------------------------------------------------------------------- /bot/command/impl/admin/admingetowner.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/i18n" 10 | "github.com/rxdn/gdl/objects/interaction" 11 | "strconv" 12 | "time" 13 | ) 14 | 15 | type AdminGetOwnerCommand struct { 16 | } 17 | 18 | func (AdminGetOwnerCommand) Properties() registry.Properties { 19 | return registry.Properties{ 20 | Name: "getowner", 21 | Description: i18n.HelpAdminGetOwner, 22 | Type: interaction.ApplicationCommandTypeChatInput, 23 | PermissionLevel: permission.Everyone, 24 | Category: command.Settings, 25 | HelperOnly: true, 26 | Arguments: command.Arguments( 27 | command.NewRequiredArgument("guild_id", "ID of the guild to get the owner of", interaction.OptionTypeString, i18n.MessageInvalidArgument), 28 | ), 29 | Timeout: time.Second * 10, 30 | } 31 | } 32 | 33 | func (c AdminGetOwnerCommand) GetExecutor() interface{} { 34 | return c.Execute 35 | } 36 | 37 | func (AdminGetOwnerCommand) Execute(ctx registry.CommandContext, raw string) { 38 | guildId, err := strconv.ParseUint(raw, 10, 64) 39 | if err != nil { 40 | ctx.ReplyRaw(customisation.Red, ctx.GetMessage(i18n.Error), "Invalid guild ID provided") 41 | return 42 | } 43 | 44 | guild, err := ctx.Worker().GetGuild(guildId) 45 | if err != nil { 46 | ctx.HandleError(err) 47 | return 48 | } 49 | 50 | ctx.ReplyRaw(customisation.Green, ctx.GetMessage(i18n.Admin), fmt.Sprintf("`%s` is owned by <@%d> (%d)", guild.Name, guild.OwnerId, guild.OwnerId)) 51 | } 52 | -------------------------------------------------------------------------------- /bot/command/impl/admin/adminlistuserentitlements.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/i18n" 11 | "github.com/rxdn/gdl/objects/channel/embed" 12 | "github.com/rxdn/gdl/objects/interaction" 13 | "time" 14 | ) 15 | 16 | type AdminListUserEntitlementsCommand struct { 17 | } 18 | 19 | func (AdminListUserEntitlementsCommand) Properties() registry.Properties { 20 | return registry.Properties{ 21 | Name: "list-user-entitlements", 22 | Description: i18n.HelpAdmin, 23 | Type: interaction.ApplicationCommandTypeChatInput, 24 | PermissionLevel: permission.Everyone, 25 | Category: command.Settings, 26 | HelperOnly: true, 27 | Arguments: command.Arguments( 28 | command.NewRequiredArgument("user", "User to fetch entitlements for", interaction.OptionTypeUser, i18n.MessageInvalidArgument), 29 | ), 30 | Timeout: time.Second * 15, 31 | } 32 | } 33 | 34 | func (c AdminListUserEntitlementsCommand) GetExecutor() interface{} { 35 | return c.Execute 36 | } 37 | 38 | func (AdminListUserEntitlementsCommand) Execute(ctx registry.CommandContext, userId uint64) { 39 | // List entitlements that have expired in the past 30 days 40 | entitlements, err := dbclient.Client.Entitlements.ListUserSubscriptions(ctx, userId, time.Hour*24*30) 41 | if err != nil { 42 | ctx.HandleError(err) 43 | return 44 | } 45 | 46 | embed := embed.NewEmbed(). 47 | SetTitle("Entitlements"). 48 | SetColor(ctx.GetColour(customisation.Blue)) 49 | 50 | if len(entitlements) == 0 { 51 | embed.SetDescription("No entitlements found") 52 | } 53 | 54 | for i, entitlement := range entitlements { 55 | if i >= 25 { 56 | embed.SetDescription("Too many entitlements to display") 57 | break 58 | } 59 | 60 | value := fmt.Sprintf( 61 | "**Tier:** %s\n**Source:** %s\n**Expires:** \n**SKU ID:** %s\n**SKU Priority:** %d", 62 | entitlement.Tier, 63 | entitlement.Source, 64 | entitlement.ExpiresAt.Unix(), 65 | entitlement.SkuId.String(), 66 | entitlement.SkuPriority, 67 | ) 68 | 69 | embed.AddField(entitlement.SkuLabel, value, false) 70 | } 71 | 72 | ctx.ReplyWithEmbed(embed) 73 | } 74 | -------------------------------------------------------------------------------- /bot/command/impl/admin/adminrecache.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "errors" 5 | "github.com/TicketsBot/common/permission" 6 | w "github.com/TicketsBot/worker" 7 | "github.com/TicketsBot/worker/bot/command" 8 | "github.com/TicketsBot/worker/bot/command/registry" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/i18n" 11 | "github.com/rxdn/gdl/objects/interaction" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | type AdminRecacheCommand struct { 17 | } 18 | 19 | func (AdminRecacheCommand) Properties() registry.Properties { 20 | return registry.Properties{ 21 | Name: "recache", 22 | Description: i18n.HelpAdmin, 23 | Type: interaction.ApplicationCommandTypeChatInput, 24 | PermissionLevel: permission.Everyone, 25 | Category: command.Settings, 26 | HelperOnly: true, 27 | Arguments: command.Arguments( 28 | command.NewOptionalArgument("guildid", "ID of the guild to recache", interaction.OptionTypeString, i18n.MessageInvalidArgument), 29 | ), 30 | Timeout: time.Second * 10, 31 | } 32 | } 33 | 34 | func (c AdminRecacheCommand) GetExecutor() interface{} { 35 | return c.Execute 36 | } 37 | 38 | func (AdminRecacheCommand) Execute(ctx registry.CommandContext, providedGuildId *string) { 39 | var guildId uint64 40 | if providedGuildId != nil { 41 | var err error 42 | guildId, err = strconv.ParseUint(*providedGuildId, 10, 64) 43 | if err != nil { 44 | ctx.HandleError(err) 45 | return 46 | } 47 | } else { 48 | guildId = ctx.GuildId() 49 | } 50 | 51 | // purge cache 52 | ctx.Worker().Cache.DeleteGuild(ctx, guildId) 53 | ctx.Worker().Cache.DeleteGuildChannels(ctx, guildId) 54 | ctx.Worker().Cache.DeleteGuildRoles(ctx, guildId) 55 | 56 | // re-cache 57 | botId, isWhitelabel, err := dbclient.Client.WhitelabelGuilds.GetBotByGuild(ctx, guildId) 58 | if err != nil { 59 | ctx.HandleError(err) 60 | return 61 | } 62 | 63 | var worker *w.Context 64 | if isWhitelabel { 65 | bot, err := dbclient.Client.Whitelabel.GetByBotId(ctx, botId) 66 | if err != nil { 67 | ctx.HandleError(err) 68 | return 69 | } 70 | 71 | if bot.BotId == 0 { 72 | ctx.HandleError(errors.New("bot not found")) 73 | return 74 | } 75 | 76 | worker = &w.Context{ 77 | Token: bot.Token, 78 | BotId: bot.BotId, 79 | IsWhitelabel: true, 80 | ShardId: 0, 81 | Cache: ctx.Worker().Cache, 82 | RateLimiter: nil, // Use http-proxy ratelimit functionality 83 | } 84 | } else { 85 | worker = ctx.Worker() 86 | } 87 | 88 | if _, err := worker.GetGuild(guildId); err != nil { 89 | ctx.HandleError(err) 90 | return 91 | } 92 | 93 | if _, err := worker.GetGuildChannels(guildId); err != nil { 94 | ctx.HandleError(err) 95 | return 96 | } 97 | 98 | ctx.ReplyPlainPermanent("done") 99 | } 100 | -------------------------------------------------------------------------------- /bot/command/impl/admin/adminwhitelabelassignguild.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/i18n" 11 | "github.com/rxdn/gdl/objects/interaction" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | type AdminWhitelabelAssignGuildCommand struct { 17 | } 18 | 19 | func (AdminWhitelabelAssignGuildCommand) Properties() registry.Properties { 20 | return registry.Properties{ 21 | Name: "whitelabel-assign-guild", 22 | Description: i18n.HelpAdmin, 23 | Type: interaction.ApplicationCommandTypeChatInput, 24 | PermissionLevel: permission.Everyone, 25 | Category: command.Settings, 26 | HelperOnly: true, 27 | Arguments: command.Arguments( 28 | command.NewRequiredArgument("bot_id", "ID of the bot to assign to the guild", interaction.OptionTypeString, i18n.MessageInvalidArgument), 29 | command.NewRequiredArgument("guild_id", "ID of the guild to assign the bot to", interaction.OptionTypeString, i18n.MessageInvalidArgument), 30 | ), 31 | Timeout: time.Second * 10, 32 | } 33 | } 34 | 35 | func (c AdminWhitelabelAssignGuildCommand) GetExecutor() interface{} { 36 | return c.Execute 37 | } 38 | 39 | func (AdminWhitelabelAssignGuildCommand) Execute(ctx registry.CommandContext, botIdRaw, guildIdRaw string) { 40 | botId, err := strconv.ParseUint(botIdRaw, 10, 64) 41 | if err != nil { 42 | ctx.ReplyRaw(customisation.Red, ctx.GetMessage(i18n.Error), "Invalid bot ID") 43 | return 44 | } 45 | 46 | guildId, err := strconv.ParseUint(guildIdRaw, 10, 64) 47 | if err != nil { 48 | ctx.ReplyRaw(customisation.Red, ctx.GetMessage(i18n.Error), "Invalid guild ID") 49 | return 50 | } 51 | 52 | bot, err := dbclient.Client.Whitelabel.GetByBotId(ctx, botId) 53 | if err != nil { 54 | ctx.HandleError(err) 55 | return 56 | } 57 | 58 | if bot.BotId == 0 { 59 | ctx.ReplyRaw(customisation.Red, ctx.GetMessage(i18n.Error), "Whitelabel bot with provided ID not found") 60 | return 61 | } 62 | 63 | if err := dbclient.Client.WhitelabelGuilds.Add(ctx, botId, guildId); err != nil { 64 | ctx.HandleError(err) 65 | return 66 | } 67 | 68 | ctx.ReplyRaw(customisation.Green, ctx.GetMessage(i18n.Success), fmt.Sprintf("Assigned bot `%d` to guild `%d`", botId, guildId)) 69 | } 70 | -------------------------------------------------------------------------------- /bot/command/impl/general/about.go: -------------------------------------------------------------------------------- 1 | package general 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/bot/customisation" 8 | "github.com/TicketsBot/worker/i18n" 9 | "github.com/rxdn/gdl/objects/interaction" 10 | "time" 11 | ) 12 | 13 | type AboutCommand struct { 14 | } 15 | 16 | func (AboutCommand) Properties() registry.Properties { 17 | return registry.Properties{ 18 | Name: "about", 19 | Description: i18n.HelpAbout, 20 | Type: interaction.ApplicationCommandTypeChatInput, 21 | PermissionLevel: permission.Everyone, 22 | Category: command.General, 23 | MainBotOnly: true, 24 | DefaultEphemeral: true, 25 | Timeout: time.Second * 3, 26 | } 27 | } 28 | 29 | func (c AboutCommand) GetExecutor() interface{} { 30 | return c.Execute 31 | } 32 | 33 | func (AboutCommand) Execute(ctx registry.CommandContext) { 34 | ctx.Reply(customisation.Green, i18n.TitleAbout, i18n.MessageAbout) 35 | } 36 | -------------------------------------------------------------------------------- /bot/command/impl/general/invite.go: -------------------------------------------------------------------------------- 1 | package general 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/bot/customisation" 8 | "github.com/TicketsBot/worker/i18n" 9 | "github.com/rxdn/gdl/objects/interaction" 10 | "time" 11 | ) 12 | 13 | type InviteCommand struct { 14 | } 15 | 16 | func (InviteCommand) Properties() registry.Properties { 17 | return registry.Properties{ 18 | Name: "invite", 19 | Description: i18n.MessageHelpInvite, 20 | Type: interaction.ApplicationCommandTypeChatInput, 21 | PermissionLevel: permission.Everyone, 22 | Category: command.General, 23 | MainBotOnly: true, 24 | DefaultEphemeral: true, 25 | Timeout: time.Second * 3, 26 | } 27 | } 28 | 29 | func (c InviteCommand) GetExecutor() interface{} { 30 | return c.Execute 31 | } 32 | 33 | func (InviteCommand) Execute(ctx registry.CommandContext) { 34 | ctx.Reply(customisation.Green, i18n.TitleInvite, i18n.MessageInvite) 35 | } 36 | -------------------------------------------------------------------------------- /bot/command/impl/general/jumptotop.go: -------------------------------------------------------------------------------- 1 | package general 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/bot/utils" 11 | "github.com/TicketsBot/worker/i18n" 12 | "github.com/rxdn/gdl/objects/interaction" 13 | "github.com/rxdn/gdl/objects/interaction/component" 14 | "time" 15 | ) 16 | 17 | type JumpToTopCommand struct { 18 | } 19 | 20 | func (JumpToTopCommand) Properties() registry.Properties { 21 | return registry.Properties{ 22 | Name: "jumptotop", 23 | Description: i18n.HelpJumpToTop, 24 | Type: interaction.ApplicationCommandTypeChatInput, 25 | PermissionLevel: permission.Everyone, 26 | Category: command.General, 27 | DefaultEphemeral: true, 28 | Timeout: time.Second * 5, 29 | } 30 | } 31 | 32 | func (c JumpToTopCommand) GetExecutor() interface{} { 33 | return c.Execute 34 | } 35 | 36 | func (JumpToTopCommand) Execute(ctx registry.CommandContext) { 37 | ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, ctx.ChannelId(), ctx.GuildId()) 38 | if err != nil { 39 | ctx.HandleError(err) 40 | return 41 | } 42 | 43 | if ticket.Id == 0 { 44 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageNotATicketChannel) 45 | return 46 | } 47 | 48 | if ticket.WelcomeMessageId == nil { 49 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageJumpToTopNoWelcomeMessage) 50 | return 51 | } 52 | 53 | messageLink := fmt.Sprintf("https://discord.com/channels/%d/%d/%d", ctx.GuildId(), ctx.ChannelId(), *ticket.WelcomeMessageId) 54 | 55 | embed := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleJumpToTop, i18n.MessageJumpToTopContent, nil) 56 | res := command.NewEphemeralEmbedMessageResponse(embed) 57 | res.Components = []component.Component{ 58 | component.BuildActionRow(component.BuildButton(component.Button{ 59 | Label: ctx.GetMessage(i18n.ClickHere), 60 | Style: component.ButtonStyleLink, 61 | Emoji: nil, 62 | Url: utils.Ptr(messageLink), 63 | Disabled: false, 64 | })), 65 | } 66 | 67 | if _, err := ctx.ReplyWith(res); err != nil { 68 | ctx.HandleError(err) 69 | return 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /bot/command/impl/settings/addadmin.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "fmt" 5 | permcache "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/context" 8 | "github.com/TicketsBot/worker/bot/command/registry" 9 | "github.com/TicketsBot/worker/bot/customisation" 10 | "github.com/TicketsBot/worker/bot/utils" 11 | "github.com/TicketsBot/worker/i18n" 12 | "github.com/rxdn/gdl/objects/channel/embed" 13 | "github.com/rxdn/gdl/objects/interaction" 14 | "github.com/rxdn/gdl/objects/interaction/component" 15 | "time" 16 | ) 17 | 18 | type AddAdminCommand struct{} 19 | 20 | func (AddAdminCommand) Properties() registry.Properties { 21 | return registry.Properties{ 22 | Name: "addadmin", 23 | Description: i18n.HelpAddAdmin, 24 | Type: interaction.ApplicationCommandTypeChatInput, 25 | PermissionLevel: permcache.Admin, 26 | Category: command.Settings, 27 | InteractionOnly: true, 28 | Arguments: command.Arguments( 29 | command.NewRequiredArgument("user_or_role", "User or role to apply the administrator permission to", interaction.OptionTypeMentionable, i18n.MessageAddAdminNoMembers), 30 | ), 31 | DefaultEphemeral: true, 32 | Timeout: time.Second * 3, 33 | } 34 | } 35 | 36 | func (c AddAdminCommand) GetExecutor() interface{} { 37 | return c.Execute 38 | } 39 | 40 | func (c AddAdminCommand) Execute(ctx registry.CommandContext, id uint64) { 41 | usageEmbed := embed.EmbedField{ 42 | Name: "Usage", 43 | Value: "`/addadmin @User`\n`/addadmin @Role`", 44 | Inline: false, 45 | } 46 | 47 | mentionableType, valid := context.DetermineMentionableType(ctx, id) 48 | if !valid { 49 | ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageAddSupportNoMembers, utils.ToSlice(usageEmbed)) 50 | return 51 | } 52 | 53 | var mention string 54 | if mentionableType == context.MentionableTypeUser { 55 | mention = fmt.Sprintf("<@%d>", id) 56 | } else if mentionableType == context.MentionableTypeRole { 57 | mention = fmt.Sprintf("<@&%d>", id) 58 | } else { 59 | ctx.HandleError(fmt.Errorf("unknown mentionable type: %d", mentionableType)) 60 | return 61 | } 62 | 63 | // Send confirmation message 64 | e := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleAddAdmin, i18n.MessageAddAdminConfirm, nil, mention) 65 | res := command.NewEphemeralEmbedMessageResponseWithComponents(e, utils.Slice(component.BuildActionRow( 66 | component.BuildButton(component.Button{ 67 | Label: ctx.GetMessage(i18n.Confirm), 68 | CustomId: fmt.Sprintf("addadmin-%d-%d", mentionableType, id), 69 | Style: component.ButtonStylePrimary, 70 | Emoji: nil, 71 | }), 72 | ))) 73 | 74 | if _, err := ctx.ReplyWith(res); err != nil { 75 | ctx.HandleError(err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /bot/command/impl/settings/addsupport.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "fmt" 5 | permcache "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/context" 8 | "github.com/TicketsBot/worker/bot/command/registry" 9 | "github.com/TicketsBot/worker/bot/customisation" 10 | "github.com/TicketsBot/worker/bot/utils" 11 | "github.com/TicketsBot/worker/i18n" 12 | "github.com/rxdn/gdl/objects/channel/embed" 13 | "github.com/rxdn/gdl/objects/interaction" 14 | "github.com/rxdn/gdl/objects/interaction/component" 15 | "time" 16 | ) 17 | 18 | type AddSupportCommand struct{} 19 | 20 | func (AddSupportCommand) Properties() registry.Properties { 21 | return registry.Properties{ 22 | Name: "addsupport", 23 | Description: i18n.HelpAddSupport, 24 | Type: interaction.ApplicationCommandTypeChatInput, 25 | Aliases: []string{"addsuport"}, 26 | PermissionLevel: permcache.Admin, 27 | Category: command.Settings, 28 | InteractionOnly: true, 29 | Arguments: command.Arguments( 30 | command.NewRequiredArgument("role", "Role to apply the support representative permission to", interaction.OptionTypeMentionable, i18n.MessageAddSupportNoMembers), 31 | ), 32 | DefaultEphemeral: true, 33 | Timeout: time.Second * 3, 34 | } 35 | } 36 | 37 | func (c AddSupportCommand) GetExecutor() interface{} { 38 | return c.Execute 39 | } 40 | 41 | func (c AddSupportCommand) Execute(ctx registry.CommandContext, id uint64) { 42 | usageEmbed := embed.EmbedField{ 43 | Name: "Usage", 44 | Value: "`/addsupport @Role`", 45 | Inline: false, 46 | } 47 | 48 | mentionableType, valid := context.DetermineMentionableType(ctx, id) 49 | if !valid { 50 | ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageAddSupportNoMembers, utils.ToSlice(usageEmbed)) 51 | return 52 | } 53 | 54 | var mention string 55 | if mentionableType == context.MentionableTypeUser { 56 | ctx.ReplyRaw(customisation.Red, "Error", "Users in support teams are now deprecated. Please use roles instead.") 57 | return 58 | 59 | //mention = fmt.Sprintf("<@%d>", id) 60 | } else if mentionableType == context.MentionableTypeRole { 61 | mention = fmt.Sprintf("<@&%d>", id) 62 | } else { 63 | ctx.HandleError(fmt.Errorf("unknown mentionable type: %d", mentionableType)) 64 | return 65 | } 66 | 67 | // Send confirmation message 68 | e := utils.BuildEmbed(ctx, customisation.Green, i18n.TitleAddSupport, i18n.MessageAddSupportConfirm, nil, mention) 69 | res := command.NewEphemeralEmbedMessageResponseWithComponents(e, utils.Slice(component.BuildActionRow( 70 | component.BuildButton(component.Button{ 71 | Label: ctx.GetMessage(i18n.Confirm), 72 | CustomId: fmt.Sprintf("addsupport-%d-%d", mentionableType, id), 73 | Style: component.ButtonStylePrimary, 74 | Emoji: nil, 75 | }), 76 | ))) 77 | 78 | if _, err := ctx.ReplyWith(res); err != nil { 79 | ctx.HandleError(err) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /bot/command/impl/settings/autoclose.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/i18n" 8 | "github.com/rxdn/gdl/objects/interaction" 9 | ) 10 | 11 | type AutoCloseCommand struct { 12 | } 13 | 14 | func (AutoCloseCommand) Properties() registry.Properties { 15 | return registry.Properties{ 16 | Name: "autoclose", 17 | Description: i18n.HelpAutoClose, 18 | Type: interaction.ApplicationCommandTypeChatInput, 19 | PermissionLevel: permission.Support, 20 | Category: command.Settings, 21 | Children: []registry.Command{ 22 | AutoCloseConfigureCommand{}, 23 | AutoCloseExcludeCommand{}, 24 | }, 25 | } 26 | } 27 | 28 | func (c AutoCloseCommand) GetExecutor() interface{} { 29 | return c.Execute 30 | } 31 | 32 | func (AutoCloseCommand) Execute(ctx registry.CommandContext) { 33 | // Can't call a parent command 34 | } 35 | -------------------------------------------------------------------------------- /bot/command/impl/settings/autocloseconfigure.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/bot/customisation" 8 | "github.com/TicketsBot/worker/i18n" 9 | "github.com/rxdn/gdl/objects/interaction" 10 | "time" 11 | ) 12 | 13 | type AutoCloseConfigureCommand struct { 14 | } 15 | 16 | func (AutoCloseConfigureCommand) Properties() registry.Properties { 17 | return registry.Properties{ 18 | Name: "configure", 19 | Description: i18n.HelpAutoCloseConfigure, 20 | Type: interaction.ApplicationCommandTypeChatInput, 21 | PermissionLevel: permission.Admin, 22 | Category: command.Settings, 23 | DefaultEphemeral: true, 24 | Timeout: time.Second * 3, 25 | } 26 | } 27 | 28 | func (c AutoCloseConfigureCommand) GetExecutor() interface{} { 29 | return c.Execute 30 | } 31 | 32 | func (AutoCloseConfigureCommand) Execute(ctx registry.CommandContext) { 33 | ctx.Reply(customisation.Green, i18n.TitleAutoclose, i18n.MessageAutoCloseConfigure, ctx.GuildId()) 34 | } 35 | -------------------------------------------------------------------------------- /bot/command/impl/settings/autocloseexclude.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/bot/customisation" 8 | "github.com/TicketsBot/worker/bot/dbclient" 9 | "github.com/TicketsBot/worker/i18n" 10 | "github.com/rxdn/gdl/objects/interaction" 11 | "time" 12 | ) 13 | 14 | type AutoCloseExcludeCommand struct { 15 | } 16 | 17 | func (AutoCloseExcludeCommand) Properties() registry.Properties { 18 | return registry.Properties{ 19 | Name: "exclude", 20 | Description: i18n.HelpAutoCloseExclude, 21 | Type: interaction.ApplicationCommandTypeChatInput, 22 | PermissionLevel: permission.Support, 23 | Category: command.Settings, 24 | DefaultEphemeral: true, 25 | Timeout: time.Second * 5, 26 | } 27 | } 28 | 29 | func (c AutoCloseExcludeCommand) GetExecutor() interface{} { 30 | return c.Execute 31 | } 32 | 33 | func (AutoCloseExcludeCommand) Execute(ctx registry.CommandContext) { 34 | ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, ctx.ChannelId(), ctx.GuildId()) 35 | if err != nil { 36 | ctx.HandleError(err) 37 | return 38 | } 39 | 40 | if ticket.Id == 0 { 41 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageNotATicketChannel) 42 | return 43 | } 44 | 45 | if err := dbclient.Client.AutoCloseExclude.Exclude(ctx, ctx.GuildId(), ticket.Id); err != nil { 46 | ctx.HandleError(err) 47 | return 48 | } 49 | 50 | ctx.Reply(customisation.Green, i18n.TitleAutoclose, i18n.MessageAutoCloseExclude) 51 | } 52 | -------------------------------------------------------------------------------- /bot/command/impl/settings/panel.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/bot/customisation" 8 | "github.com/TicketsBot/worker/i18n" 9 | "github.com/rxdn/gdl/objects/interaction" 10 | "time" 11 | ) 12 | 13 | type PanelCommand struct { 14 | } 15 | 16 | func (PanelCommand) Properties() registry.Properties { 17 | return registry.Properties{ 18 | Name: "panel", 19 | Description: i18n.HelpPanel, 20 | Type: interaction.ApplicationCommandTypeChatInput, 21 | PermissionLevel: permission.Admin, 22 | Category: command.Settings, 23 | DefaultEphemeral: true, 24 | Timeout: time.Second * 3, 25 | } 26 | } 27 | 28 | func (c PanelCommand) GetExecutor() interface{} { 29 | return c.Execute 30 | } 31 | 32 | func (PanelCommand) Execute(ctx registry.CommandContext) { 33 | ctx.Reply(customisation.Green, i18n.TitlePanel, i18n.MessagePanel, ctx.GuildId()) 34 | } 35 | -------------------------------------------------------------------------------- /bot/command/impl/settings/setup/limit.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/bot/customisation" 8 | "github.com/TicketsBot/worker/bot/dbclient" 9 | "github.com/TicketsBot/worker/i18n" 10 | "github.com/rxdn/gdl/objects/interaction" 11 | "time" 12 | ) 13 | 14 | type LimitSetupCommand struct{} 15 | 16 | func (LimitSetupCommand) Properties() registry.Properties { 17 | return registry.Properties{ 18 | Name: "limit", 19 | Description: i18n.HelpSetup, 20 | Type: interaction.ApplicationCommandTypeChatInput, 21 | PermissionLevel: permission.Admin, 22 | Category: command.Settings, 23 | Arguments: command.Arguments( 24 | command.NewRequiredArgument("limit", "The maximum amount of tickets a user can have open simultaneously", interaction.OptionTypeInteger, i18n.SetupLimitInvalid), 25 | ), 26 | Timeout: time.Second * 3, 27 | } 28 | } 29 | 30 | func (c LimitSetupCommand) GetExecutor() interface{} { 31 | return c.Execute 32 | } 33 | 34 | func (LimitSetupCommand) Execute(ctx registry.CommandContext, limit int) { 35 | if limit < 1 || limit > 10 { 36 | ctx.Reply(customisation.Red, i18n.TitleSetup, i18n.SetupLimitInvalid) 37 | return 38 | } 39 | 40 | if err := dbclient.Client.TicketLimit.Set(ctx, ctx.GuildId(), uint8(limit)); err != nil { 41 | ctx.HandleError(err) 42 | return 43 | } 44 | 45 | ctx.Reply(customisation.Green, i18n.TitleSetup, i18n.SetupLimitComplete, limit) 46 | } 47 | -------------------------------------------------------------------------------- /bot/command/impl/settings/setup/setup.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/i18n" 8 | "github.com/rxdn/gdl/objects/interaction" 9 | ) 10 | 11 | type SetupCommand struct { 12 | } 13 | 14 | func (SetupCommand) Properties() registry.Properties { 15 | return registry.Properties{ 16 | Name: "setup", 17 | Description: i18n.HelpSetup, 18 | Type: interaction.ApplicationCommandTypeChatInput, 19 | PermissionLevel: permission.Admin, 20 | Category: command.Settings, 21 | Children: []registry.Command{ 22 | AutoSetupCommand{}, 23 | LimitSetupCommand{}, 24 | TranscriptsSetupCommand{}, 25 | ThreadsSetupCommand{}, 26 | }, 27 | } 28 | } 29 | 30 | func (c SetupCommand) GetExecutor() interface{} { 31 | return c.Execute 32 | } 33 | 34 | func (c SetupCommand) Execute(ctx registry.CommandContext) { 35 | // Parent commands cannot be called 36 | //ctx.ReplyWithFieldsPermanent(customisation.Green, i18n.TitleSetup, i18n.SetupChoose, c.buildFields(ctx)) 37 | } 38 | 39 | /* TODO: Remove 40 | func (SetupCommand) buildFields(ctx registry.CommandContext) []embed.EmbedField { 41 | fields := make([]embed.EmbedField, 9) 42 | 43 | group, _ := errgroup.WithContext(context.Background()) 44 | 45 | group.Go(getFieldFunc(ctx, fields, 0, "/setup auto", i18n.SetupAutoDescription, true)) 46 | group.Go(getFieldFunc(ctx, fields, 1, "Dashboard", i18n.SetupDashboardDescription, true)) 47 | fields[2] = embed.EmbedField{ 48 | Name: "\u200b", 49 | Value: "‎", 50 | Inline: true, 51 | } 52 | group.Go(getFieldFunc(ctx, fields, 3, "/setup prefix", i18n.SetupPrefixDescription, true)) 53 | group.Go(getFieldFunc(ctx, fields, 4, "/setup limit", i18n.SetupLimitDescription, true)) 54 | group.Go(getFieldFunc(ctx, fields, 5, "/setup welcomemessage", i18n.SetupWelcomeMessageDescription, false)) 55 | group.Go(getFieldFunc(ctx, fields, 6, "/setup transcripts", i18n.SetupTranscriptsDescription, true)) 56 | group.Go(getFieldFunc(ctx, fields, 7, "/setup category", i18n.SetupCategoryDescription, true)) 57 | group.Go(getFieldFunc(ctx, fields, 8, "Reaction Panels", i18n.SetupReactionPanelsDescription, false, ctx.GuildId)) 58 | 59 | // should never happen 60 | if err := group.Wait(); err != nil { 61 | sentry.Error(err) 62 | return nil 63 | } 64 | 65 | return fields 66 | } 67 | 68 | func newFieldFromTranslation(ctx registry.CommandContext, name string, value i18n.MessageId, inline bool, format ...interface{}) embed.EmbedField { 69 | return embed.EmbedField{ 70 | Name: name, 71 | Value: i18n.GetMessageFromGuild(ctx.GuildId(), value, format...), 72 | Inline: inline, 73 | } 74 | } 75 | 76 | func getFieldFunc(ctx registry.CommandContext, fields []embed.EmbedField, index int, name string, value i18n.MessageId, inline bool, format ...interface{}) func() error { 77 | return func() error { 78 | fields[index] = newFieldFromTranslation(ctx, name, value, inline, format...) 79 | return nil 80 | } 81 | } 82 | */ 83 | -------------------------------------------------------------------------------- /bot/command/impl/settings/setup/threads.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/bot/customisation" 8 | "github.com/TicketsBot/worker/bot/dbclient" 9 | "github.com/TicketsBot/worker/i18n" 10 | "github.com/rxdn/gdl/objects/channel" 11 | "github.com/rxdn/gdl/objects/interaction" 12 | "time" 13 | ) 14 | 15 | type ThreadsSetupCommand struct{} 16 | 17 | func (ThreadsSetupCommand) Properties() registry.Properties { 18 | return registry.Properties{ 19 | Name: "use-threads", 20 | Description: i18n.HelpSetup, 21 | Type: interaction.ApplicationCommandTypeChatInput, 22 | PermissionLevel: permission.Admin, 23 | Category: command.Settings, 24 | Arguments: command.Arguments( 25 | command.NewRequiredArgument("use_threads", "Whether or not private threads should be used for ticket", interaction.OptionTypeBoolean, "infallible"), 26 | command.NewOptionalArgument("ticket_notification_channel", "The channel that ticket open notifications should be sent to", interaction.OptionTypeChannel, "infallible"), 27 | ), 28 | InteractionOnly: true, 29 | Timeout: time.Second * 5, 30 | } 31 | } 32 | 33 | func (c ThreadsSetupCommand) GetExecutor() interface{} { 34 | return c.Execute 35 | } 36 | 37 | func (ThreadsSetupCommand) Execute(ctx registry.CommandContext, useThreads bool, channelId *uint64) { 38 | if useThreads { 39 | if channelId == nil { 40 | ctx.Reply(customisation.Red, i18n.Error, i18n.SetupThreadsNoNotificationChannel) 41 | return 42 | } 43 | 44 | ch, err := ctx.Worker().GetChannel(*channelId) 45 | if err != nil { 46 | ctx.HandleError(err) 47 | return 48 | } 49 | 50 | if ch.Type != channel.ChannelTypeGuildText { 51 | ctx.Reply(customisation.Red, i18n.Error, i18n.SetupThreadsNotificationChannelType) 52 | return 53 | } 54 | 55 | if err := dbclient.Client.Settings.EnableThreads(ctx, ctx.GuildId(), *channelId); err != nil { 56 | ctx.HandleError(err) 57 | return 58 | } 59 | 60 | ctx.Reply(customisation.Green, i18n.TitleSetup, i18n.SetupThreadsSuccess) 61 | } else { 62 | if err := dbclient.Client.Settings.DisableThreads(ctx, ctx.GuildId()); err != nil { 63 | ctx.HandleError(err) 64 | return 65 | } 66 | 67 | ctx.Reply(customisation.Green, i18n.TitleSetup, i18n.SetupThreadsDisabled) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /bot/command/impl/settings/setup/transcripts.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/bot/customisation" 8 | "github.com/TicketsBot/worker/bot/dbclient" 9 | "github.com/TicketsBot/worker/bot/utils" 10 | "github.com/TicketsBot/worker/i18n" 11 | "github.com/rxdn/gdl/objects/interaction" 12 | "github.com/rxdn/gdl/rest/request" 13 | "time" 14 | ) 15 | 16 | type TranscriptsSetupCommand struct{} 17 | 18 | func (TranscriptsSetupCommand) Properties() registry.Properties { 19 | return registry.Properties{ 20 | Name: "transcripts", 21 | Description: i18n.HelpSetup, 22 | Type: interaction.ApplicationCommandTypeChatInput, 23 | Aliases: []string{"transcript", "archives", "archive"}, 24 | PermissionLevel: permission.Admin, 25 | Category: command.Settings, 26 | Arguments: command.Arguments( 27 | command.NewRequiredArgument("channel", "The channel that ticket transcripts should be sent to", interaction.OptionTypeChannel, i18n.SetupTranscriptsInvalid), 28 | ), 29 | Timeout: time.Second * 5, 30 | } 31 | } 32 | 33 | func (c TranscriptsSetupCommand) GetExecutor() interface{} { 34 | return c.Execute 35 | } 36 | 37 | func (TranscriptsSetupCommand) Execute(ctx registry.CommandContext, channelId uint64) { 38 | if _, err := ctx.Worker().GetChannel(channelId); err != nil { 39 | if restError, ok := err.(request.RestError); ok && restError.IsClientError() { 40 | ctx.Reply(customisation.Red, i18n.Error, i18n.SetupTranscriptsInvalid, ctx.ChannelId) 41 | } else { 42 | ctx.HandleError(err) 43 | } 44 | 45 | return 46 | } 47 | 48 | if err := dbclient.Client.ArchiveChannel.Set(ctx, ctx.GuildId(), utils.Ptr(channelId)); err == nil { 49 | ctx.Reply(customisation.Green, i18n.TitleSetup, i18n.SetupTranscriptsComplete, channelId) 50 | } else { 51 | ctx.HandleError(err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /bot/command/impl/settings/viewstaff.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/bot/logic" 8 | "github.com/TicketsBot/worker/i18n" 9 | "github.com/rxdn/gdl/objects/channel/embed" 10 | "github.com/rxdn/gdl/objects/channel/message" 11 | "github.com/rxdn/gdl/objects/guild/emoji" 12 | "github.com/rxdn/gdl/objects/interaction" 13 | "github.com/rxdn/gdl/objects/interaction/component" 14 | "time" 15 | ) 16 | 17 | type ViewStaffCommand struct { 18 | } 19 | 20 | func (ViewStaffCommand) Properties() registry.Properties { 21 | return registry.Properties{ 22 | Name: "viewstaff", 23 | Description: i18n.HelpViewStaff, 24 | Type: interaction.ApplicationCommandTypeChatInput, 25 | PermissionLevel: permission.Everyone, 26 | Category: command.Settings, 27 | DefaultEphemeral: true, 28 | Timeout: time.Second * 5, 29 | } 30 | } 31 | 32 | func (c ViewStaffCommand) GetExecutor() interface{} { 33 | return c.Execute 34 | } 35 | 36 | func (ViewStaffCommand) Execute(ctx registry.CommandContext) { 37 | msgEmbed, _ := logic.BuildViewStaffMessage(ctx, ctx, 0) 38 | 39 | res := command.MessageResponse{ 40 | Embeds: []*embed.Embed{msgEmbed}, 41 | Flags: message.SumFlags(message.FlagEphemeral), 42 | Components: []component.Component{ 43 | component.BuildActionRow( 44 | component.BuildButton(component.Button{ 45 | CustomId: "disabled", 46 | Style: component.ButtonStylePrimary, 47 | Emoji: &emoji.Emoji{ 48 | Name: "◀️", 49 | }, 50 | Disabled: true, 51 | }), 52 | component.BuildButton(component.Button{ 53 | CustomId: "viewstaff_1", 54 | Style: component.ButtonStylePrimary, 55 | Emoji: &emoji.Emoji{ 56 | Name: "▶️", 57 | }, 58 | Disabled: false, 59 | }), 60 | ), 61 | }, 62 | } 63 | 64 | _, _ = ctx.ReplyWith(res) 65 | } 66 | -------------------------------------------------------------------------------- /bot/command/impl/statistics/stats.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/i18n" 8 | "github.com/rxdn/gdl/objects/interaction" 9 | ) 10 | 11 | type StatsCommand struct { 12 | } 13 | 14 | func (StatsCommand) Properties() registry.Properties { 15 | return registry.Properties{ 16 | Name: "stats", 17 | Description: i18n.HelpStats, 18 | Type: interaction.ApplicationCommandTypeChatInput, 19 | Aliases: []string{"statistics"}, 20 | PermissionLevel: permission.Support, 21 | Children: []registry.Command{ 22 | StatsUserCommand{}, 23 | StatsServerCommand{}, 24 | }, 25 | Category: command.Statistics, 26 | PremiumOnly: true, 27 | } 28 | } 29 | 30 | func (c StatsCommand) GetExecutor() interface{} { 31 | return c.Execute 32 | } 33 | 34 | func (StatsCommand) Execute(ctx registry.CommandContext) { 35 | // Cannot call parent command 36 | } 37 | -------------------------------------------------------------------------------- /bot/command/impl/tags/managetags.go: -------------------------------------------------------------------------------- 1 | package tags 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/i18n" 8 | "github.com/rxdn/gdl/objects/interaction" 9 | ) 10 | 11 | type ManageTagsCommand struct { 12 | } 13 | 14 | func (ManageTagsCommand) Properties() registry.Properties { 15 | return registry.Properties{ 16 | Name: "managetags", 17 | Description: i18n.HelpManageTags, 18 | Type: interaction.ApplicationCommandTypeChatInput, 19 | Aliases: []string{"managecannedresponse", "managecannedresponses", "editcannedresponse", "editcannedresponses", "ecr", "managetags", "mcr", "managetag", "mt"}, 20 | PermissionLevel: permission.Support, 21 | Children: []registry.Command{ 22 | ManageTagsAddCommand{}, 23 | ManageTagsDeleteCommand{}, 24 | ManageTagsListCommand{}, 25 | }, 26 | Category: command.Tags, 27 | DefaultEphemeral: true, 28 | } 29 | } 30 | 31 | func (c ManageTagsCommand) GetExecutor() interface{} { 32 | return c.Execute 33 | } 34 | 35 | func (ManageTagsCommand) Execute(_ registry.CommandContext) { 36 | // Cannot call parent command 37 | } 38 | -------------------------------------------------------------------------------- /bot/command/impl/tags/managetagsadd.go: -------------------------------------------------------------------------------- 1 | package tags 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/database" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/bot/utils" 11 | "github.com/TicketsBot/worker/i18n" 12 | "github.com/rxdn/gdl/objects/channel/embed" 13 | "github.com/rxdn/gdl/objects/interaction" 14 | "time" 15 | ) 16 | 17 | type ManageTagsAddCommand struct { 18 | } 19 | 20 | func (ManageTagsAddCommand) Properties() registry.Properties { 21 | return registry.Properties{ 22 | Name: "add", 23 | Description: i18n.HelpTagAdd, 24 | Type: interaction.ApplicationCommandTypeChatInput, 25 | Aliases: []string{"new", "create"}, 26 | PermissionLevel: permission.Support, 27 | Category: command.Tags, 28 | InteractionOnly: true, 29 | Arguments: command.Arguments( 30 | command.NewRequiredArgument("id", "Identifier for the tag", interaction.OptionTypeString, i18n.MessageTagCreateInvalidArguments), 31 | command.NewRequiredArgument("content", "Tag contents to be sent when /tag is used", interaction.OptionTypeString, i18n.MessageTagCreateInvalidArguments), 32 | ), 33 | DefaultEphemeral: true, 34 | Timeout: time.Second * 3, 35 | } 36 | } 37 | 38 | func (c ManageTagsAddCommand) GetExecutor() interface{} { 39 | return c.Execute 40 | } 41 | 42 | func (ManageTagsAddCommand) Execute(ctx registry.CommandContext, tagId, content string) { 43 | usageEmbed := embed.EmbedField{ 44 | Name: "Usage", 45 | Value: "`/managetags add [TagID] [Tag Contents]`", 46 | Inline: false, 47 | } 48 | 49 | // Limit of 200 tags 50 | count, err := dbclient.Client.Tag.GetTagCount(ctx, ctx.GuildId()) 51 | if err != nil { 52 | ctx.HandleError(err) 53 | return 54 | } 55 | 56 | if count >= 200 { 57 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageTagCreateLimit, 200) 58 | return 59 | } 60 | 61 | // Length check 62 | if len(tagId) > 16 { 63 | ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageTagCreateTooLong, utils.ToSlice(usageEmbed)) 64 | return 65 | } 66 | 67 | // Verify a tag with the ID doesn't already exist 68 | exists, err := dbclient.Client.Tag.Exists(ctx, ctx.GuildId(), tagId) 69 | if err != nil { 70 | ctx.HandleError(err) 71 | return 72 | } 73 | 74 | if exists { 75 | ctx.ReplyWithFields(customisation.Red, i18n.Error, i18n.MessageTagCreateAlreadyExists, utils.ToSlice(usageEmbed), tagId, tagId) 76 | return 77 | } 78 | 79 | tag := database.Tag{ 80 | Id: tagId, 81 | GuildId: ctx.GuildId(), 82 | Content: &content, 83 | Embed: nil, 84 | ApplicationCommandId: nil, 85 | } 86 | 87 | if err := dbclient.Client.Tag.Set(ctx, tag); err != nil { 88 | ctx.HandleError(err) 89 | return 90 | } 91 | 92 | ctx.Reply(customisation.Green, i18n.MessageTag, i18n.MessageTagCreateSuccess, tagId) 93 | } 94 | -------------------------------------------------------------------------------- /bot/command/impl/tags/managetagsdelete.go: -------------------------------------------------------------------------------- 1 | package tags 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/bot/customisation" 8 | "github.com/TicketsBot/worker/bot/dbclient" 9 | "github.com/TicketsBot/worker/i18n" 10 | "github.com/rxdn/gdl/objects/interaction" 11 | "time" 12 | ) 13 | 14 | type ManageTagsDeleteCommand struct { 15 | } 16 | 17 | func (ManageTagsDeleteCommand) Properties() registry.Properties { 18 | return registry.Properties{ 19 | Name: "delete", 20 | Description: i18n.HelpTagDelete, 21 | Type: interaction.ApplicationCommandTypeChatInput, 22 | Aliases: []string{"del", "rm", "remove"}, 23 | PermissionLevel: permission.Support, 24 | Category: command.Tags, 25 | Arguments: command.Arguments( 26 | command.NewRequiredArgument("id", "ID of the tag to delete", interaction.OptionTypeString, i18n.MessageTagDeleteInvalidArguments), 27 | ), 28 | DefaultEphemeral: true, 29 | Timeout: time.Second * 3, 30 | } 31 | } 32 | 33 | func (c ManageTagsDeleteCommand) GetExecutor() interface{} { 34 | return c.Execute 35 | } 36 | 37 | func (ManageTagsDeleteCommand) Execute(ctx registry.CommandContext, tagId string) { 38 | exists, err := dbclient.Client.Tag.Exists(ctx, ctx.GuildId(), tagId) 39 | if err != nil { 40 | ctx.HandleError(err) 41 | return 42 | } 43 | 44 | if !exists { 45 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageTagDeleteDoesNotExist, tagId) 46 | return 47 | } 48 | 49 | if err := dbclient.Client.Tag.Delete(ctx, ctx.GuildId(), tagId); err != nil { 50 | ctx.HandleError(err) 51 | return 52 | } 53 | 54 | ctx.Reply(customisation.Green, i18n.MessageTag, i18n.MessageTagDeleteSuccess, tagId) 55 | } 56 | -------------------------------------------------------------------------------- /bot/command/impl/tags/managetagslist.go: -------------------------------------------------------------------------------- 1 | package tags 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/i18n" 11 | "github.com/rxdn/gdl/objects/interaction" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | type ManageTagsListCommand struct { 17 | } 18 | 19 | func (ManageTagsListCommand) Properties() registry.Properties { 20 | return registry.Properties{ 21 | Name: "list", 22 | Description: i18n.HelpTagList, 23 | Type: interaction.ApplicationCommandTypeChatInput, 24 | PermissionLevel: permission.Support, 25 | Category: command.Tags, 26 | DefaultEphemeral: true, 27 | Timeout: time.Second * 3, 28 | } 29 | } 30 | 31 | func (c ManageTagsListCommand) GetExecutor() interface{} { 32 | return c.Execute 33 | } 34 | 35 | func (ManageTagsListCommand) Execute(ctx registry.CommandContext) { 36 | ids, err := dbclient.Client.Tag.GetTagIds(ctx, ctx.GuildId()) 37 | if err != nil { 38 | ctx.HandleError(err) 39 | return 40 | } 41 | 42 | var joined string 43 | for _, id := range ids { 44 | joined += fmt.Sprintf("• `%s`\n", id) 45 | } 46 | joined = strings.TrimSuffix(joined, "\n") 47 | 48 | ctx.Reply(customisation.Green, i18n.TitleTags, i18n.MessageTagList, joined, "/") 49 | } 50 | -------------------------------------------------------------------------------- /bot/command/impl/tags/tagalias.go: -------------------------------------------------------------------------------- 1 | package tags 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/common/premium" 6 | "github.com/TicketsBot/common/sentry" 7 | "github.com/TicketsBot/database" 8 | "github.com/TicketsBot/worker/bot/command" 9 | "github.com/TicketsBot/worker/bot/command/registry" 10 | "github.com/TicketsBot/worker/bot/customisation" 11 | "github.com/TicketsBot/worker/bot/dbclient" 12 | "github.com/TicketsBot/worker/bot/logic" 13 | "github.com/TicketsBot/worker/bot/utils" 14 | "github.com/TicketsBot/worker/i18n" 15 | "github.com/rxdn/gdl/objects/channel/embed" 16 | "github.com/rxdn/gdl/objects/channel/message" 17 | "github.com/rxdn/gdl/objects/interaction" 18 | "time" 19 | ) 20 | 21 | type TagAliasCommand struct { 22 | tag database.Tag 23 | } 24 | 25 | func NewTagAliasCommand(tag database.Tag) TagAliasCommand { 26 | return TagAliasCommand{ 27 | tag: tag, 28 | } 29 | } 30 | 31 | func (c TagAliasCommand) Properties() registry.Properties { 32 | return registry.Properties{ 33 | Name: c.tag.Id, 34 | Description: i18n.HelpTag, 35 | Type: interaction.ApplicationCommandTypeChatInput, 36 | PermissionLevel: permission.Everyone, 37 | Category: command.Tags, 38 | Timeout: time.Second * 5, 39 | } 40 | } 41 | 42 | func (c TagAliasCommand) GetExecutor() interface{} { 43 | return c.Execute 44 | } 45 | 46 | func (c TagAliasCommand) Execute(ctx registry.CommandContext) { 47 | if ctx.PremiumTier() < premium.Premium { 48 | ctx.Reply(customisation.Red, i18n.TitlePremiumOnly, i18n.MessageTagAliasRequiresPremium) 49 | return 50 | } 51 | 52 | ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, ctx.ChannelId(), ctx.GuildId()) 53 | if err != nil { 54 | sentry.ErrorWithContext(err, ctx.ToErrorContext()) 55 | return 56 | } 57 | 58 | // Count user as a participant so that Tickets Answered stat includes tickets where only /tag was used 59 | if ticket.GuildId != 0 { 60 | go func() { 61 | if err := dbclient.Client.Participants.Set(ctx, ctx.GuildId(), ticket.Id, ctx.UserId()); err != nil { 62 | sentry.ErrorWithContext(err, ctx.ToErrorContext()) 63 | } 64 | }() 65 | } 66 | 67 | content := utils.ValueOrZero(c.tag.Content) 68 | if ticket.Id != 0 { 69 | content = logic.DoPlaceholderSubstitutions(ctx, content, ctx.Worker(), ticket, nil) 70 | } 71 | 72 | var embeds []*embed.Embed 73 | if c.tag.Embed != nil { 74 | embeds = []*embed.Embed{ 75 | logic.BuildCustomEmbed(ctx, ctx.Worker(), ticket, *c.tag.Embed.CustomEmbed, c.tag.Embed.Fields, false, nil), 76 | } 77 | } 78 | 79 | var allowedMentions message.AllowedMention 80 | if ticket.Id != 0 { 81 | allowedMentions = message.AllowedMention{ 82 | Users: []uint64{ticket.UserId}, 83 | } 84 | } 85 | 86 | data := command.MessageResponse{ 87 | Content: content, 88 | Embeds: embeds, 89 | AllowedMentions: allowedMentions, 90 | } 91 | 92 | if _, err := ctx.ReplyWith(data); err != nil { 93 | ctx.HandleError(err) 94 | return 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /bot/command/impl/tickets/claim.go: -------------------------------------------------------------------------------- 1 | package tickets 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/constants" 9 | "github.com/TicketsBot/worker/bot/customisation" 10 | "github.com/TicketsBot/worker/bot/dbclient" 11 | "github.com/TicketsBot/worker/bot/logic" 12 | "github.com/TicketsBot/worker/i18n" 13 | "github.com/rxdn/gdl/objects/channel" 14 | "github.com/rxdn/gdl/objects/interaction" 15 | ) 16 | 17 | type ClaimCommand struct { 18 | } 19 | 20 | func (ClaimCommand) Properties() registry.Properties { 21 | return registry.Properties{ 22 | Name: "claim", 23 | Description: i18n.HelpClaim, 24 | Type: interaction.ApplicationCommandTypeChatInput, 25 | PermissionLevel: permission.Support, 26 | Category: command.Tickets, 27 | Timeout: constants.TimeoutOpenTicket, 28 | } 29 | } 30 | 31 | func (c ClaimCommand) GetExecutor() interface{} { 32 | return c.Execute 33 | } 34 | 35 | func (ClaimCommand) Execute(ctx registry.CommandContext) { 36 | // Get ticket struct 37 | ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, ctx.ChannelId(), ctx.GuildId()) 38 | if err != nil { 39 | ctx.HandleError(err) 40 | return 41 | } 42 | 43 | // Verify this is a ticket channel 44 | if ticket.UserId == 0 { 45 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageNotATicketChannel) 46 | return 47 | } 48 | 49 | // Check if thread 50 | ch, err := ctx.Worker().GetChannel(ctx.ChannelId()) 51 | if err != nil { 52 | ctx.HandleError(err) 53 | return 54 | } 55 | 56 | if ch.Type == channel.ChannelTypeGuildPrivateThread { 57 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageClaimThread) 58 | return 59 | } 60 | 61 | if err := logic.ClaimTicket(ctx, ctx, ticket, ctx.UserId()); err != nil { 62 | ctx.HandleError(err) 63 | return 64 | } 65 | 66 | ctx.ReplyPermanent(customisation.Green, i18n.TitleClaimed, i18n.MessageClaimed, fmt.Sprintf("<@%d>", ctx.UserId())) 67 | } 68 | -------------------------------------------------------------------------------- /bot/command/impl/tickets/close.go: -------------------------------------------------------------------------------- 1 | package tickets 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/common/sentry" 7 | "github.com/TicketsBot/worker/bot/command" 8 | "github.com/TicketsBot/worker/bot/command/registry" 9 | "github.com/TicketsBot/worker/bot/constants" 10 | "github.com/TicketsBot/worker/bot/dbclient" 11 | "github.com/TicketsBot/worker/bot/logic" 12 | "github.com/TicketsBot/worker/bot/utils" 13 | "github.com/TicketsBot/worker/i18n" 14 | "github.com/rxdn/gdl/objects/interaction" 15 | "time" 16 | ) 17 | 18 | type CloseCommand struct { 19 | } 20 | 21 | func (c CloseCommand) Properties() registry.Properties { 22 | return registry.Properties{ 23 | Name: "close", 24 | Description: i18n.HelpClose, 25 | Type: interaction.ApplicationCommandTypeChatInput, 26 | PermissionLevel: permission.Everyone, 27 | Category: command.Tickets, 28 | Arguments: command.Arguments( 29 | command.NewOptionalAutocompleteableArgument("reason", "The reason the ticket was closed", interaction.OptionTypeString, "infallible", c.AutoCompleteHandler), // should never fail 30 | ), 31 | Timeout: constants.TimeoutCloseTicket, 32 | } 33 | } 34 | 35 | func (c CloseCommand) GetExecutor() interface{} { 36 | return c.Execute 37 | } 38 | 39 | func (CloseCommand) Execute(ctx registry.CommandContext, reason *string) { 40 | logic.CloseTicket(ctx, ctx, reason, false) 41 | } 42 | 43 | func (CloseCommand) AutoCompleteHandler(data interaction.ApplicationCommandAutoCompleteInteraction, value string) []interaction.ApplicationCommandOptionChoice { 44 | var reasons []string 45 | var err error 46 | 47 | // Get ticket 48 | ticket, e := dbclient.Client.Tickets.GetByChannelAndGuild(context.Background(), data.ChannelId, data.GuildId.Value) 49 | if e != nil { 50 | sentry.Error(e) // TODO: Context 51 | return nil 52 | } 53 | 54 | ctx, cancel := utils.ContextTimeout(time.Millisecond * 1500) 55 | defer cancel() 56 | 57 | // If there is no text provided by the user yet, and this is a ticket channel, we can use our materialised view to 58 | // get the most common close reasons for that panel. Otherwise, perform a dynamic query to get the most common 59 | // reasons for that text for all panels. 60 | if len(value) == 0 { 61 | var panelId *int 62 | if ticket.Id != 0 { 63 | panelId = ticket.PanelId 64 | } 65 | 66 | reasons, err = dbclient.Analytics.GetTopCloseReasons(ctx, data.GuildId.Value, panelId) 67 | } else { 68 | var panelId *int 69 | if ticket.Id != 0 { 70 | panelId = ticket.PanelId 71 | } 72 | 73 | reasons, err = dbclient.Analytics.GetTopCloseReasonsWithPrefix(ctx, data.GuildId.Value, panelId, value) 74 | } 75 | 76 | if err != nil { 77 | sentry.Error(err) // TODO: Context 78 | return nil 79 | } 80 | 81 | choices := make([]interaction.ApplicationCommandOptionChoice, len(reasons)) 82 | for i, reason := range reasons { 83 | choices[i] = utils.StringChoice(reason) 84 | } 85 | 86 | return choices 87 | } 88 | -------------------------------------------------------------------------------- /bot/command/impl/tickets/open.go: -------------------------------------------------------------------------------- 1 | package tickets 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/context" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/constants" 9 | "github.com/TicketsBot/worker/bot/customisation" 10 | "github.com/TicketsBot/worker/bot/logic" 11 | "github.com/TicketsBot/worker/i18n" 12 | "github.com/rxdn/gdl/objects/interaction" 13 | ) 14 | 15 | type OpenCommand struct { 16 | } 17 | 18 | func (OpenCommand) Properties() registry.Properties { 19 | return registry.Properties{ 20 | Name: "open", 21 | Description: i18n.HelpOpen, 22 | Type: interaction.ApplicationCommandTypeChatInput, 23 | Aliases: []string{"new"}, 24 | PermissionLevel: permission.Everyone, 25 | Category: command.Tickets, 26 | Arguments: command.Arguments( 27 | command.NewOptionalArgument("subject", "The subject of the ticket", interaction.OptionTypeString, "infallible"), 28 | ), 29 | DefaultEphemeral: true, 30 | Timeout: constants.TimeoutOpenTicket, 31 | } 32 | } 33 | 34 | func (c OpenCommand) GetExecutor() interface{} { 35 | return c.Execute 36 | } 37 | 38 | func (OpenCommand) Execute(ctx *context.SlashCommandContext, providedSubject *string) { 39 | settings, err := ctx.Settings() 40 | if err != nil { 41 | ctx.HandleError(err) 42 | return 43 | } 44 | 45 | if settings.DisableOpenCommand { 46 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageOpenCommandDisabled) 47 | return 48 | } 49 | 50 | var subject string 51 | if providedSubject != nil { 52 | subject = *providedSubject 53 | } 54 | 55 | logic.OpenTicket(ctx.Context, ctx, nil, subject, nil) 56 | } 57 | -------------------------------------------------------------------------------- /bot/command/impl/tickets/rename.go: -------------------------------------------------------------------------------- 1 | package tickets 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/bot/command/registry" 7 | "github.com/TicketsBot/worker/bot/customisation" 8 | "github.com/TicketsBot/worker/bot/dbclient" 9 | "github.com/TicketsBot/worker/bot/redis" 10 | "github.com/TicketsBot/worker/bot/utils" 11 | "github.com/TicketsBot/worker/i18n" 12 | "github.com/rxdn/gdl/objects/channel/embed" 13 | "github.com/rxdn/gdl/objects/interaction" 14 | "github.com/rxdn/gdl/rest" 15 | "time" 16 | ) 17 | 18 | type RenameCommand struct { 19 | } 20 | 21 | func (RenameCommand) Properties() registry.Properties { 22 | return registry.Properties{ 23 | Name: "rename", 24 | Description: i18n.HelpRename, 25 | Type: interaction.ApplicationCommandTypeChatInput, 26 | PermissionLevel: permission.Support, 27 | Category: command.Tickets, 28 | Arguments: command.Arguments( 29 | command.NewRequiredArgument("name", "New name for the ticket", interaction.OptionTypeString, i18n.MessageRenameMissingName), 30 | ), 31 | DefaultEphemeral: true, 32 | Timeout: time.Second * 5, 33 | } 34 | } 35 | 36 | func (c RenameCommand) GetExecutor() interface{} { 37 | return c.Execute 38 | } 39 | 40 | func (RenameCommand) Execute(ctx registry.CommandContext, name string) { 41 | usageEmbed := embed.EmbedField{ 42 | Name: "Usage", 43 | Value: "`/rename [ticket-name]`", 44 | Inline: false, 45 | } 46 | 47 | ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, ctx.ChannelId(), ctx.GuildId()) 48 | if err != nil { 49 | ctx.HandleError(err) 50 | return 51 | } 52 | 53 | // Check this is a ticket channel 54 | if ticket.UserId == 0 { 55 | ctx.ReplyWithFields(customisation.Red, i18n.TitleRename, i18n.MessageNotATicketChannel, utils.ToSlice(usageEmbed)) 56 | return 57 | } 58 | 59 | if len(name) > 100 { 60 | ctx.Reply(customisation.Red, i18n.TitleRename, i18n.MessageRenameTooLong) 61 | return 62 | } 63 | 64 | allowed, err := redis.TakeRenameRatelimit(ctx, ctx.ChannelId()) 65 | if err != nil { 66 | ctx.HandleError(err) 67 | return 68 | } 69 | 70 | if !allowed { 71 | ctx.Reply(customisation.Red, i18n.TitleRename, i18n.MessageRenameRatelimited) 72 | return 73 | } 74 | 75 | data := rest.ModifyChannelData{ 76 | Name: name, 77 | } 78 | 79 | if _, err := ctx.Worker().ModifyChannel(ctx.ChannelId(), data); err != nil { 80 | ctx.HandleError(err) 81 | return 82 | } 83 | 84 | ctx.Reply(customisation.Green, i18n.TitleRename, i18n.MessageRenamed, ctx.ChannelId()) 85 | } 86 | -------------------------------------------------------------------------------- /bot/command/impl/tickets/reopen.go: -------------------------------------------------------------------------------- 1 | package tickets 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/common/sentry" 7 | "github.com/TicketsBot/worker/bot/command" 8 | "github.com/TicketsBot/worker/bot/command/registry" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/bot/logic" 11 | "github.com/TicketsBot/worker/i18n" 12 | "github.com/rxdn/gdl/objects/interaction" 13 | "strconv" 14 | "time" 15 | ) 16 | 17 | type ReopenCommand struct { 18 | } 19 | 20 | func (c ReopenCommand) Properties() registry.Properties { 21 | return registry.Properties{ 22 | Name: "reopen", 23 | Description: i18n.HelpReopen, 24 | Type: interaction.ApplicationCommandTypeChatInput, 25 | PermissionLevel: permission.Everyone, 26 | Category: command.Tickets, 27 | Arguments: command.Arguments( 28 | command.NewRequiredAutocompleteableArgument("ticket_id", "ID of the ticket to reopen", interaction.OptionTypeInteger, i18n.MessageInvalidArgument, c.AutoCompleteHandler), 29 | ), 30 | DefaultEphemeral: true, 31 | Timeout: time.Second * 10, 32 | } 33 | } 34 | 35 | func (c ReopenCommand) GetExecutor() interface{} { 36 | return c.Execute 37 | } 38 | 39 | func (ReopenCommand) Execute(ctx registry.CommandContext, ticketId int) { 40 | logic.ReopenTicket(ctx, ctx, ticketId) 41 | } 42 | 43 | func (ReopenCommand) AutoCompleteHandler(data interaction.ApplicationCommandAutoCompleteInteraction, value string) []interaction.ApplicationCommandOptionChoice { 44 | if data.GuildId.Value == 0 { 45 | return nil 46 | } 47 | 48 | if data.Member == nil { 49 | return nil 50 | } 51 | 52 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) // TODO: Propagate contxet 53 | defer cancel() 54 | 55 | tickets, err := dbclient.Client.Tickets.GetClosedByUserPrefixed(ctx, data.GuildId.Value, data.Member.User.Id, value, 25) 56 | if err != nil { 57 | sentry.Error(err) 58 | return nil 59 | } 60 | 61 | choices := make([]interaction.ApplicationCommandOptionChoice, len(tickets)) 62 | for i, ticket := range tickets { 63 | if i >= 25 { // Infallible 64 | break 65 | } 66 | 67 | choices[i] = interaction.ApplicationCommandOptionChoice{ 68 | Name: strconv.Itoa(ticket.Id), 69 | Value: ticket.Id, 70 | } 71 | } 72 | 73 | return choices 74 | } 75 | -------------------------------------------------------------------------------- /bot/command/impl/tickets/transfer.go: -------------------------------------------------------------------------------- 1 | package tickets 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/constants" 9 | "github.com/TicketsBot/worker/bot/customisation" 10 | "github.com/TicketsBot/worker/bot/dbclient" 11 | "github.com/TicketsBot/worker/bot/logic" 12 | "github.com/TicketsBot/worker/bot/utils" 13 | "github.com/TicketsBot/worker/i18n" 14 | "github.com/rxdn/gdl/objects/channel" 15 | "github.com/rxdn/gdl/objects/interaction" 16 | ) 17 | 18 | type TransferCommand struct { 19 | } 20 | 21 | func (TransferCommand) Properties() registry.Properties { 22 | return registry.Properties{ 23 | Name: "transfer", 24 | Description: i18n.HelpTransfer, 25 | Type: interaction.ApplicationCommandTypeChatInput, 26 | PermissionLevel: permission.Support, 27 | Category: command.Tickets, 28 | Arguments: command.Arguments( 29 | command.NewRequiredArgument("user", "Support representative to transfer the ticket to", interaction.OptionTypeUser, i18n.MessageInvalidUser), 30 | ), 31 | Timeout: constants.TimeoutOpenTicket, 32 | } 33 | } 34 | 35 | func (c TransferCommand) GetExecutor() interface{} { 36 | return c.Execute 37 | } 38 | 39 | func (TransferCommand) Execute(ctx registry.CommandContext, userId uint64) { 40 | // Get ticket struct 41 | ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, ctx.ChannelId(), ctx.GuildId()) 42 | if err != nil { 43 | ctx.HandleError(err) 44 | return 45 | } 46 | 47 | // Verify this is a ticket channel 48 | if ticket.UserId == 0 { 49 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageNotATicketChannel) 50 | return 51 | } 52 | 53 | // Check if thread 54 | ch, err := ctx.Channel() 55 | if err != nil { 56 | ctx.HandleError(err) 57 | return 58 | } 59 | 60 | if ch.Type == channel.ChannelTypeGuildPrivateThread { 61 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageClaimThread) 62 | return 63 | } 64 | 65 | member, err := ctx.Worker().GetGuildMember(ctx.GuildId(), userId) 66 | if err != nil { 67 | ctx.HandleError(err) 68 | return 69 | } 70 | 71 | permissionLevel, err := permission.GetPermissionLevel(ctx, utils.ToRetriever(ctx.Worker()), member, ctx.GuildId()) 72 | if err != nil { 73 | ctx.HandleError(err) 74 | return 75 | } 76 | 77 | if permissionLevel < permission.Support { 78 | ctx.Reply(customisation.Red, i18n.Error, i18n.MessageInvalidUser) 79 | return 80 | } 81 | 82 | if err := logic.ClaimTicket(ctx, ctx, ticket, userId); err != nil { 83 | ctx.HandleError(err) 84 | return 85 | } 86 | 87 | ctx.ReplyPermanent(customisation.Green, i18n.TitleClaim, i18n.MessageClaimed, fmt.Sprintf("<@%d>", userId)) 88 | } 89 | -------------------------------------------------------------------------------- /bot/command/mentionabletype.go: -------------------------------------------------------------------------------- 1 | package command 2 | -------------------------------------------------------------------------------- /bot/command/registry/command.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/worker/i18n" 6 | "strings" 7 | ) 8 | 9 | type Command interface { 10 | GetExecutor() interface{} 11 | //Execute(ctx CommandContext) 12 | Properties() Properties 13 | } 14 | 15 | func FormatHelp(c Command, guildId uint64, commandId *uint64) string { 16 | description := i18n.GetMessageFromGuild(guildId, c.Properties().Description) 17 | 18 | if commandId == nil { 19 | var args []string 20 | for _, arg := range c.Properties().Arguments { 21 | if arg.Required { 22 | args = append(args, fmt.Sprintf("[%s] ", arg.Name)) 23 | } else { 24 | args = append(args, fmt.Sprintf("<%s> ", arg.Name)) 25 | } 26 | } 27 | 28 | var argsJoined string 29 | if len(args) > 0 { 30 | argsJoined = " " + strings.Join(args, " ") // Separate between command and first arg 31 | } 32 | 33 | return fmt.Sprintf("**/%s%s**: %s", c.Properties().Name, argsJoined, description) 34 | } else { 35 | return fmt.Sprintf(": %s", c.Properties().Name, *commandId, description) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bot/command/registry/commandcontext.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | permcache "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/common/premium" 6 | "github.com/TicketsBot/database" 7 | "github.com/TicketsBot/worker" 8 | "github.com/TicketsBot/worker/bot/command" 9 | "github.com/TicketsBot/worker/bot/customisation" 10 | "github.com/TicketsBot/worker/bot/errorcontext" 11 | "github.com/TicketsBot/worker/i18n" 12 | "github.com/rxdn/gdl/objects/channel" 13 | "github.com/rxdn/gdl/objects/channel/embed" 14 | "github.com/rxdn/gdl/objects/channel/message" 15 | "github.com/rxdn/gdl/objects/guild" 16 | "github.com/rxdn/gdl/objects/guild/emoji" 17 | "github.com/rxdn/gdl/objects/interaction" 18 | "github.com/rxdn/gdl/objects/member" 19 | "github.com/rxdn/gdl/objects/user" 20 | "golang.org/x/net/context" 21 | ) 22 | 23 | type CommandContext interface { 24 | context.Context 25 | 26 | Worker() *worker.Context 27 | 28 | GuildId() uint64 29 | ChannelId() uint64 30 | UserId() uint64 31 | 32 | UserPermissionLevel(ctx context.Context) (permcache.PermissionLevel, error) 33 | PremiumTier() premium.PremiumTier 34 | IsInteraction() bool 35 | Source() Source 36 | ToErrorContext() errorcontext.WorkerErrorContext 37 | 38 | Reply(colour customisation.Colour, title, content i18n.MessageId, format ...interface{}) 39 | ReplyWith(response command.MessageResponse) (message.Message, error) 40 | ReplyWithEmbed(embed *embed.Embed) 41 | ReplyWithEmbedPermanent(embed *embed.Embed) 42 | ReplyPermanent(colour customisation.Colour, title, content i18n.MessageId, format ...interface{}) 43 | ReplyWithFields(colour customisation.Colour, title, content i18n.MessageId, fields []embed.EmbedField, format ...interface{}) 44 | ReplyWithFieldsPermanent(colour customisation.Colour, title, content i18n.MessageId, fields []embed.EmbedField, format ...interface{}) 45 | 46 | ReplyRaw(colour customisation.Colour, title, content string) 47 | ReplyRawPermanent(colour customisation.Colour, title, content string) 48 | 49 | ReplyPlain(content string) 50 | ReplyPlainPermanent(content string) 51 | 52 | SelectValidEmoji(customEmoji customisation.CustomEmoji, fallback string) *emoji.Emoji 53 | 54 | HandleError(err error) 55 | HandleWarning(err error) 56 | 57 | GetMessage(messageId i18n.MessageId, format ...interface{}) string 58 | GetColour(colour customisation.Colour) int 59 | 60 | // Utility functions 61 | Channel() (channel.PartialChannel, error) 62 | Guild() (guild.Guild, error) 63 | Member() (member.Member, error) 64 | User() (user.User, error) 65 | Settings() (database.Settings, error) 66 | 67 | IsBlacklisted(ctx context.Context) (bool, error) 68 | } 69 | 70 | type InteractionContext interface { 71 | CommandContext 72 | InteractionMetadata() interaction.InteractionMetadata 73 | } 74 | -------------------------------------------------------------------------------- /bot/command/registry/properties.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "github.com/TicketsBot/common/permission" 5 | "github.com/TicketsBot/worker/bot/command" 6 | "github.com/TicketsBot/worker/i18n" 7 | "github.com/rxdn/gdl/objects/interaction" 8 | "time" 9 | ) 10 | 11 | type Properties struct { 12 | Name string 13 | Description i18n.MessageId 14 | Type interaction.ApplicationCommandType 15 | Aliases []string 16 | PermissionLevel permission.PermissionLevel 17 | Children []Command // TODO: Map 18 | PremiumOnly bool 19 | Category command.Category 20 | AdminOnly bool 21 | HelperOnly bool 22 | InteractionOnly bool 23 | MessageOnly bool 24 | MainBotOnly bool 25 | Arguments []command.Argument 26 | DefaultEphemeral bool 27 | Timeout time.Duration 28 | 29 | SetupFunc func() 30 | } 31 | -------------------------------------------------------------------------------- /bot/command/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | type Registry map[string]Command 4 | -------------------------------------------------------------------------------- /bot/command/registry/source.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | type Source uint8 4 | 5 | const ( 6 | SourceDiscord Source = iota 7 | SourceDashboard 8 | SourceAutoClose 9 | ) 10 | -------------------------------------------------------------------------------- /bot/constants/timeouts.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import "time" 4 | 5 | const ( 6 | TimeoutCloseTicket = time.Second * 15 7 | TimeoutOpenTicket = time.Second * 22 8 | ) 9 | -------------------------------------------------------------------------------- /bot/customisation/colour.go: -------------------------------------------------------------------------------- 1 | package customisation 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/sentry" 6 | "github.com/TicketsBot/worker/bot/dbclient" 7 | ) 8 | 9 | type Colour int16 10 | 11 | func (c Colour) Int16() int16 { 12 | return int16(c) 13 | } 14 | 15 | func (c Colour) Default() int { 16 | return DefaultColours[c] 17 | } 18 | 19 | const ( 20 | Green Colour = iota 21 | Red 22 | Orange 23 | Lime 24 | Blue 25 | ) 26 | 27 | var DefaultColours = map[Colour]int{ 28 | Green: 0x2ECC71, 29 | Red: 0xFC3F35, 30 | Orange: 16740864, 31 | Lime: 7658240, 32 | Blue: 472219, 33 | } 34 | 35 | func GetDefaultColour(colour Colour) int { 36 | return DefaultColours[colour] 37 | } 38 | 39 | func IsValidColour(colour Colour) bool { 40 | _, valid := DefaultColours[colour] 41 | return valid 42 | } 43 | 44 | func GetColours(ctx context.Context, guildId uint64) (map[Colour]int, error) { 45 | raw, err := dbclient.Client.CustomColours.GetAll(ctx, guildId) 46 | if err != nil { 47 | return DefaultColours, err 48 | } 49 | 50 | colours := make(map[Colour]int) 51 | for id, hex := range raw { 52 | colours[Colour(id)] = hex 53 | } 54 | 55 | for id, hex := range DefaultColours { 56 | if _, ok := colours[id]; !ok { 57 | colours[id] = hex 58 | } 59 | } 60 | 61 | return colours, nil 62 | } 63 | 64 | // TODO: Premium check 65 | func GetColour(ctx context.Context, guildId uint64, colourCode Colour) (int, error) { 66 | colour, ok, err := dbclient.Client.CustomColours.Get(ctx, guildId, colourCode.Int16()) 67 | if err != nil { 68 | return 0, err 69 | } 70 | 71 | if !ok { 72 | return GetDefaultColour(colourCode), nil 73 | } 74 | 75 | return colour, nil 76 | } 77 | 78 | // TODO: Premium check 79 | func GetColourOrDefault(ctx context.Context, guildId uint64, colourCode Colour) int { 80 | colour, ok, err := dbclient.Client.CustomColours.Get(ctx, guildId, colourCode.Int16()) 81 | if err != nil { 82 | sentry.Error(err) 83 | return GetDefaultColour(colourCode) 84 | } 85 | 86 | if !ok { 87 | return GetDefaultColour(colourCode) 88 | } 89 | 90 | return colour 91 | } 92 | -------------------------------------------------------------------------------- /bot/customisation/emoji.go: -------------------------------------------------------------------------------- 1 | package customisation 2 | 3 | import ( 4 | "fmt" 5 | "github.com/rxdn/gdl/objects" 6 | "github.com/rxdn/gdl/objects/guild/emoji" 7 | ) 8 | 9 | type CustomEmoji struct { 10 | Name string 11 | Id uint64 12 | Animated bool 13 | } 14 | 15 | func NewCustomEmoji(name string, id uint64, animated bool) CustomEmoji { 16 | return CustomEmoji{ 17 | Name: name, 18 | Id: id, 19 | } 20 | } 21 | 22 | func (e CustomEmoji) String() string { 23 | if e.Animated { 24 | return fmt.Sprintf("", e.Name, e.Id) 25 | } else { 26 | return fmt.Sprintf("<:%s:%d>", e.Name, e.Id) 27 | } 28 | } 29 | 30 | func (e CustomEmoji) BuildEmoji() *emoji.Emoji { 31 | return &emoji.Emoji{ 32 | Id: objects.NewNullableSnowflake(e.Id), 33 | Name: e.Name, 34 | Animated: e.Animated, 35 | } 36 | } 37 | 38 | var ( 39 | EmojiId = NewCustomEmoji("id", 1013527224722391181, false) 40 | EmojiOpen = NewCustomEmoji("open", 1013527364455649430, false) 41 | EmojiOpenTime = NewCustomEmoji("opentime", 1013527365638430790, false) 42 | EmojiClose = NewCustomEmoji("close", 1013527306192560188, false) 43 | EmojiCloseTime = NewCustomEmoji("closetime", 1013527317341012009, false) 44 | EmojiReason = NewCustomEmoji("reason", 1013527372399657023, false) 45 | EmojiSubject = NewCustomEmoji("subject", 1013527369832738907, false) 46 | EmojiTranscript = NewCustomEmoji("transcript", 1013527375327281213, false) 47 | EmojiClaim = NewCustomEmoji("claim", 1013527266124369980, false) 48 | EmojiPanel = NewCustomEmoji("panel", 1013527367265820682, false) 49 | EmojiRating = NewCustomEmoji("rating", 1013527368360538244, false) 50 | EmojiStaff = NewCustomEmoji("staff", 1013527371216867370, false) 51 | EmojiThread = NewCustomEmoji("thread", 1013527373750214717, false) 52 | EmojiBulletLine = NewCustomEmoji("bulletline", 1014161470491201596, false) 53 | EmojiPatreon = NewCustomEmoji("patreon", 1016062317210906704, false) 54 | EmojiDiscord = NewCustomEmoji("discord", 1278678797113233531, false) 55 | //EmojiTime = NewCustomEmoji("time", 974006684622159952, false) 56 | ) 57 | 58 | // PrefixWithEmoji Useful for whitelabel bots 59 | func PrefixWithEmoji(s string, emoji CustomEmoji, includeEmoji bool) string { 60 | if includeEmoji { 61 | return fmt.Sprintf("%s %s", emoji, s) 62 | } else { 63 | return s 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /bot/dbclient/analytics.go: -------------------------------------------------------------------------------- 1 | package dbclient 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/analytics-client" 6 | "github.com/TicketsBot/worker/config" 7 | "go.uber.org/zap" 8 | "time" 9 | ) 10 | 11 | var Analytics *analytics.Client 12 | 13 | func ConnectAnalytics(logger *zap.Logger) { 14 | logger.Info("Connecting to Clickhouse", 15 | zap.String("address", config.Conf.Clickhouse.Address), 16 | zap.String("database", config.Conf.Clickhouse.Database), 17 | zap.String("username", config.Conf.Clickhouse.Username), 18 | zap.Int("threads", config.Conf.Clickhouse.Threads), 19 | ) 20 | 21 | Analytics = analytics.Connect( 22 | config.Conf.Clickhouse.Address, 23 | config.Conf.Clickhouse.Threads, 24 | config.Conf.Clickhouse.Database, 25 | config.Conf.Clickhouse.Username, 26 | config.Conf.Clickhouse.Password, 27 | time.Second*10, 28 | ) 29 | 30 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 31 | defer cancel() 32 | 33 | if err := Analytics.Ping(ctx); err != nil { 34 | logger.Error("Clickhouse didn't response to ping", zap.Error(err)) 35 | return 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bot/dbclient/dbclient.go: -------------------------------------------------------------------------------- 1 | package dbclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/TicketsBot/database" 7 | "github.com/TicketsBot/worker/config" 8 | "github.com/jackc/pgx/v4" 9 | "github.com/jackc/pgx/v4/pgxpool" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | var Client *database.Database 14 | 15 | func Connect(logger *zap.Logger) { 16 | cfg, err := pgxpool.ParseConfig(fmt.Sprintf( 17 | "postgres://%s:%s@%s/%s?pool_max_conns=%d", 18 | config.Conf.Database.Username, 19 | config.Conf.Database.Password, 20 | config.Conf.Database.Host, 21 | config.Conf.Database.Database, 22 | config.Conf.Database.Threads, 23 | )) 24 | 25 | if err != nil { 26 | logger.Fatal("Failed to parse database config", zap.Error(err)) 27 | return 28 | } 29 | 30 | // TODO: Sentry 31 | cfg.ConnConfig.LogLevel = pgx.LogLevelWarn 32 | cfg.ConnConfig.Logger = NewLogAdapter(logger) 33 | 34 | pool, err := pgxpool.ConnectConfig(context.Background(), cfg) 35 | if err != nil { 36 | logger.Fatal("Failed to connect to database", zap.Error(err)) 37 | return 38 | } 39 | 40 | Client = database.NewDatabase(pool) 41 | } 42 | -------------------------------------------------------------------------------- /bot/dbclient/logger.go: -------------------------------------------------------------------------------- 1 | package dbclient 2 | 3 | import ( 4 | "context" 5 | "github.com/jackc/pgx/v4" 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | type LogAdapter struct { 11 | logger *zap.Logger 12 | } 13 | 14 | var _ pgx.Logger = (*LogAdapter)(nil) 15 | 16 | func NewLogAdapter(logger *zap.Logger) *LogAdapter { 17 | return &LogAdapter{ 18 | logger: logger, 19 | } 20 | } 21 | 22 | func (l *LogAdapter) Log(ctx context.Context, level pgx.LogLevel, msg string, data map[string]interface{}) { 23 | l.logger.Log(pgxLevelToZapLevel(level), msg, toZapFields(data)...) 24 | } 25 | 26 | func pgxLevelToZapLevel(level pgx.LogLevel) zapcore.Level { 27 | switch level { 28 | case pgx.LogLevelTrace: 29 | return zapcore.DebugLevel 30 | case pgx.LogLevelDebug: 31 | return zapcore.DebugLevel 32 | case pgx.LogLevelInfo: 33 | return zapcore.InfoLevel 34 | case pgx.LogLevelWarn: 35 | return zapcore.WarnLevel 36 | case pgx.LogLevelError: 37 | return zapcore.ErrorLevel 38 | default: 39 | return zapcore.InfoLevel 40 | } 41 | } 42 | 43 | func toZapFields(data map[string]interface{}) []zap.Field { 44 | fields := make([]zap.Field, 0, len(data)) 45 | for key, value := range data { 46 | fields = append(fields, zap.Any(key, value)) 47 | } 48 | return fields 49 | } 50 | -------------------------------------------------------------------------------- /bot/errorcontext/workererrorcontext.go: -------------------------------------------------------------------------------- 1 | package errorcontext 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type WorkerErrorContext struct { 8 | Guild uint64 9 | User uint64 10 | Channel uint64 11 | } 12 | 13 | func (w WorkerErrorContext) ToMap() map[string]string { 14 | m := make(map[string]string) 15 | 16 | if w.Guild != 0 { 17 | m["guild"] = strconv.FormatUint(w.Guild, 10) 18 | } 19 | 20 | if w.User != 0 { 21 | m["user"] = strconv.FormatUint(w.User, 10) 22 | } 23 | 24 | if w.Channel != 0 { 25 | m["channel"] = strconv.FormatUint(w.Channel, 10) 26 | } 27 | 28 | return m 29 | } 30 | -------------------------------------------------------------------------------- /bot/integrations/integrations.go: -------------------------------------------------------------------------------- 1 | package integrations 2 | 3 | import ( 4 | "github.com/TicketsBot/common/integrations/bloxlink" 5 | "github.com/TicketsBot/common/webproxy" 6 | "github.com/TicketsBot/worker/bot/redis" 7 | "github.com/TicketsBot/worker/config" 8 | ) 9 | 10 | var ( 11 | WebProxy *webproxy.WebProxy 12 | SecureProxy *SecureProxyClient 13 | Bloxlink *bloxlink.BloxlinkIntegration 14 | ) 15 | 16 | func InitIntegrations() { 17 | WebProxy = webproxy.NewWebProxy(config.Conf.WebProxy.Url, config.Conf.WebProxy.AuthHeaderName, config.Conf.WebProxy.AuthHeaderValue) 18 | Bloxlink = bloxlink.NewBloxlinkIntegration(redis.Client, WebProxy, config.Conf.Integrations.BloxlinkApiKey) 19 | SecureProxy = NewSecureProxy(config.Conf.Integrations.SecureProxyUrl) 20 | } 21 | -------------------------------------------------------------------------------- /bot/integrations/secureproxy.go: -------------------------------------------------------------------------------- 1 | package integrations 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "github.com/TicketsBot/common/sentry" 11 | "io" 12 | "net/http" 13 | ) 14 | 15 | type SecureProxyClient struct { 16 | Url string 17 | client *http.Client 18 | } 19 | 20 | func NewSecureProxy(url string) *SecureProxyClient { 21 | return &SecureProxyClient{ 22 | Url: url, 23 | client: &http.Client{}, 24 | } 25 | } 26 | 27 | type secureProxyRequest struct { 28 | Method string `json:"method"` 29 | Url string `json:"url"` 30 | Headers map[string]string `json:"headers,omitempty"` 31 | Body []byte `json:"body,omitempty"` 32 | JsonBody json.RawMessage `json:"json_body,omitempty"` 33 | } 34 | 35 | type requestBody interface { 36 | []byte | any 37 | } 38 | 39 | func (p *SecureProxyClient) DoRequest(ctx context.Context, method, url string, headers map[string]string, bodyData requestBody) ([]byte, error) { 40 | body := secureProxyRequest{ 41 | Method: method, 42 | Url: url, 43 | Headers: headers, 44 | } 45 | 46 | // nil will fall through anyway 47 | if bodyData != nil && (method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch || method == http.MethodDelete) { 48 | switch v := bodyData.(type) { 49 | case []byte: 50 | base64.StdEncoding.Encode(body.Body, v) 51 | case any: 52 | encoded, err := json.Marshal(v) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | body.JsonBody = json.RawMessage(encoded) 58 | } 59 | } 60 | 61 | encoded, err := json.Marshal(body) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.Url+"/proxy", bytes.NewBuffer(encoded)) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | req.Header.Set("Content-Type", "application/json") 72 | 73 | res, err := p.client.Do(req) 74 | if err != nil { 75 | sentry.Error(err) 76 | return nil, errors.New("error proxying request") 77 | } 78 | 79 | defer res.Body.Close() 80 | 81 | if errorHeader := res.Header.Get("x-proxy-error"); errorHeader != "" { 82 | return nil, errors.New(errorHeader) 83 | } 84 | 85 | if res.StatusCode != 200 { 86 | return nil, fmt.Errorf("integration request returned status code %d", res.StatusCode) 87 | } 88 | 89 | resBody, err := io.ReadAll(res.Body) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | return resBody, nil 95 | } 96 | -------------------------------------------------------------------------------- /bot/listeners/channeldelete.go: -------------------------------------------------------------------------------- 1 | package listeners 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/sentry" 6 | "github.com/TicketsBot/worker" 7 | "github.com/TicketsBot/worker/bot/dbclient" 8 | "github.com/rxdn/gdl/gateway/payloads/events" 9 | "time" 10 | ) 11 | 12 | func OnChannelDelete(worker *worker.Context, e events.ChannelDelete) { 13 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) // TODO: Propagate context 14 | defer cancel() 15 | 16 | // If this is a ticket channel, close it 17 | if err := sentry.WithSpan1(ctx, "Close ticket by channel", func(span *sentry.Span) error { 18 | return dbclient.Client.Tickets.CloseByChannel(ctx, e.Id) 19 | }); err != nil { 20 | sentry.Error(err) 21 | } 22 | 23 | // if this is a channel category, delete it 24 | if err := sentry.WithSpan1(ctx, "Delete category by channel", func(span *sentry.Span) error { 25 | return dbclient.Client.ChannelCategory.DeleteByChannel(ctx, e.Id) 26 | }); err != nil { 27 | sentry.Error(err) 28 | } 29 | 30 | // if this is an archive channel, delete it 31 | if err := sentry.WithSpan1(ctx, "Delete archive channel by channel", func(span *sentry.Span) error { 32 | return dbclient.Client.ArchiveChannel.DeleteByChannel(ctx, e.Id) 33 | }); err != nil { 34 | sentry.Error(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bot/listeners/guildleave.go: -------------------------------------------------------------------------------- 1 | package listeners 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/sentry" 6 | "github.com/TicketsBot/worker" 7 | "github.com/TicketsBot/worker/bot/dbclient" 8 | "github.com/TicketsBot/worker/bot/metrics/statsd" 9 | "github.com/rxdn/gdl/gateway/payloads/events" 10 | "time" 11 | ) 12 | 13 | /* 14 | * Sent when a guild becomes unavailable during a guild outage, or when the user leaves or is removed from a guild. 15 | * The inner payload is an unavailable guild object. 16 | * If the unavailable field is not set, the user was removed from the guild. 17 | */ 18 | func OnGuildLeave(worker *worker.Context, e events.GuildDelete) { 19 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) // TODO: Propagate context 20 | defer cancel() 21 | 22 | span := sentry.StartSpan(ctx, "OnGuildLeave") 23 | defer span.Finish() 24 | 25 | if e.Unavailable == nil { 26 | statsd.Client.IncrementKey(statsd.KeyLeaves) 27 | 28 | if worker.IsWhitelabel { 29 | if err := dbclient.Client.WhitelabelGuilds.Delete(ctx, worker.BotId, e.Guild.Id); err != nil { 30 | sentry.Error(err) 31 | } 32 | } 33 | 34 | // Exclude from autoclose 35 | if err := dbclient.Client.AutoCloseExclude.ExcludeAll(ctx, e.Guild.Id); err != nil { 36 | sentry.Error(err) 37 | } 38 | 39 | if err := dbclient.Client.GuildLeaveTime.Set(ctx, e.Guild.Id); err != nil { 40 | sentry.Error(err) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /bot/listeners/memberleave.go: -------------------------------------------------------------------------------- 1 | package listeners 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/sentry" 6 | "github.com/TicketsBot/worker" 7 | cmdcontext "github.com/TicketsBot/worker/bot/command/context" 8 | "github.com/TicketsBot/worker/bot/constants" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/bot/listeners/messagequeue" 11 | "github.com/TicketsBot/worker/bot/logic" 12 | "github.com/TicketsBot/worker/bot/utils" 13 | "github.com/rxdn/gdl/gateway/payloads/events" 14 | gdlUtils "github.com/rxdn/gdl/utils" 15 | "time" 16 | ) 17 | 18 | // Remove user permissions when they leave 19 | func OnMemberLeave(worker *worker.Context, e events.GuildMemberRemove) { 20 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) // TODO: Propagate context 21 | defer cancel() 22 | 23 | if err := dbclient.Client.Permissions.RemoveSupport(ctx, e.GuildId, e.User.Id); err != nil { 24 | sentry.Error(err) 25 | } 26 | 27 | if err := utils.ToRetriever(worker).Cache().DeleteCachedPermissionLevel(ctx, e.GuildId, e.User.Id); err != nil { 28 | sentry.Error(err) 29 | } 30 | 31 | // auto close 32 | settings, err := dbclient.Client.AutoClose.Get(ctx, e.GuildId) 33 | if err != nil { 34 | sentry.Error(err) 35 | } else { 36 | // check setting is enabled 37 | if settings.Enabled && settings.OnUserLeave != nil && *settings.OnUserLeave { 38 | // get open tickets by user 39 | tickets, err := dbclient.Client.Tickets.GetOpenByUser(ctx, e.GuildId, e.User.Id) 40 | if err != nil { 41 | sentry.Error(err) 42 | } else { 43 | for _, ticket := range tickets { 44 | isExcluded, err := dbclient.Client.AutoCloseExclude.IsExcluded(ctx, e.GuildId, ticket.Id) 45 | if err != nil { 46 | sentry.Error(err) 47 | continue 48 | } 49 | 50 | if isExcluded { 51 | continue 52 | } 53 | 54 | // verify ticket exists + prevent potential panic 55 | if ticket.ChannelId == nil { 56 | return 57 | } 58 | 59 | // get premium status 60 | premiumTier, err := utils.PremiumClient.GetTierByGuildId(ctx, ticket.GuildId, true, worker.Token, worker.RateLimiter) 61 | if err != nil { 62 | sentry.Error(err) 63 | return 64 | } 65 | 66 | ctx, cancel := context.WithTimeout(context.Background(), constants.TimeoutCloseTicket) 67 | 68 | cc := cmdcontext.NewAutoCloseContext(ctx, worker, e.GuildId, *ticket.ChannelId, worker.BotId, premiumTier) 69 | logic.CloseTicket(ctx, cc, gdlUtils.StrPtr(messagequeue.AutoCloseReason), true) 70 | 71 | cancel() 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /bot/listeners/memberupdate.go: -------------------------------------------------------------------------------- 1 | package listeners 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/sentry" 6 | "github.com/TicketsBot/worker" 7 | "github.com/TicketsBot/worker/bot/utils" 8 | "github.com/rxdn/gdl/gateway/payloads/events" 9 | "time" 10 | ) 11 | 12 | // Remove user permissions when they leave 13 | func OnMemberUpdate(worker *worker.Context, e events.GuildMemberUpdate) { 14 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) // TODO: Propagate context 15 | defer cancel() 16 | 17 | span := sentry.StartSpan(ctx, "OnMemberUpdate") 18 | defer span.Finish() 19 | 20 | if err := utils.ToRetriever(worker).Cache().DeleteCachedPermissionLevel(ctx, e.GuildId, e.User.Id); err != nil { 21 | sentry.Error(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bot/listeners/messagequeue/autoclose.go: -------------------------------------------------------------------------------- 1 | package messagequeue 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/autoclose" 6 | "github.com/TicketsBot/common/sentry" 7 | "github.com/TicketsBot/worker/bot/cache" 8 | cmdcontext "github.com/TicketsBot/worker/bot/command/context" 9 | "github.com/TicketsBot/worker/bot/constants" 10 | "github.com/TicketsBot/worker/bot/dbclient" 11 | "github.com/TicketsBot/worker/bot/logic" 12 | "github.com/TicketsBot/worker/bot/metrics/statsd" 13 | "github.com/TicketsBot/worker/bot/redis" 14 | "github.com/TicketsBot/worker/bot/utils" 15 | gdlUtils "github.com/rxdn/gdl/utils" 16 | ) 17 | 18 | const AutoCloseReason = "Automatically closed due to inactivity" 19 | 20 | func ListenAutoClose() { 21 | ch := make(chan autoclose.Ticket) 22 | go autoclose.Listen(redis.Client, ch) 23 | 24 | for ticket := range ch { 25 | statsd.Client.IncrementKey(statsd.AutoClose) 26 | 27 | ticket := ticket 28 | go func() { 29 | ctx, cancel := context.WithTimeout(context.Background(), constants.TimeoutCloseTicket) 30 | defer cancel() 31 | 32 | // get ticket 33 | ticket, err := dbclient.Client.Tickets.Get(ctx, ticket.TicketId, ticket.GuildId) 34 | if err != nil { 35 | sentry.Error(err) 36 | return 37 | } 38 | 39 | // get worker 40 | worker, err := buildContext(ctx, ticket, cache.Client) 41 | if err != nil { 42 | sentry.Error(err) 43 | return 44 | } 45 | 46 | // query already checks, but just to be sure 47 | if ticket.ChannelId == nil { 48 | return 49 | } 50 | 51 | // get premium status 52 | premiumTier, err := utils.PremiumClient.GetTierByGuildId(ctx, ticket.GuildId, true, worker.Token, worker.RateLimiter) 53 | if err != nil { 54 | sentry.Error(err) 55 | return 56 | } 57 | 58 | cc := cmdcontext.NewAutoCloseContext(ctx, worker, ticket.GuildId, *ticket.ChannelId, worker.BotId, premiumTier) 59 | logic.CloseTicket(ctx, cc, gdlUtils.StrPtr(AutoCloseReason), true) 60 | }() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /bot/listeners/messagequeue/closerequesttimer.go: -------------------------------------------------------------------------------- 1 | package messagequeue 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/closerequest" 6 | "github.com/TicketsBot/common/sentry" 7 | "github.com/TicketsBot/database" 8 | "github.com/TicketsBot/worker/bot/cache" 9 | cmdcontext "github.com/TicketsBot/worker/bot/command/context" 10 | "github.com/TicketsBot/worker/bot/constants" 11 | "github.com/TicketsBot/worker/bot/dbclient" 12 | "github.com/TicketsBot/worker/bot/logic" 13 | "github.com/TicketsBot/worker/bot/metrics/statsd" 14 | "github.com/TicketsBot/worker/bot/redis" 15 | "github.com/TicketsBot/worker/bot/utils" 16 | ) 17 | 18 | func ListenCloseRequestTimer() { 19 | ch := make(chan database.CloseRequest) 20 | go closerequest.Listen(redis.Client, ch) 21 | 22 | for request := range ch { 23 | statsd.Client.IncrementKey(statsd.AutoClose) 24 | 25 | request := request 26 | go func() { 27 | ctx, cancel := context.WithTimeout(context.Background(), constants.TimeoutCloseTicket) 28 | defer cancel() 29 | 30 | // get ticket 31 | ticket, err := dbclient.Client.Tickets.Get(ctx, request.TicketId, request.GuildId) 32 | if err != nil { 33 | sentry.Error(err) 34 | return 35 | } 36 | 37 | // get worker 38 | worker, err := buildContext(ctx, ticket, cache.Client) 39 | if err != nil { 40 | sentry.Error(err) 41 | return 42 | } 43 | 44 | // query already checks, but just to be sure 45 | if ticket.ChannelId == nil { 46 | return 47 | } 48 | 49 | // get premium status 50 | premiumTier, err := utils.PremiumClient.GetTierByGuildId(ctx, ticket.GuildId, true, worker.Token, worker.RateLimiter) 51 | if err != nil { 52 | sentry.Error(err) 53 | return 54 | } 55 | 56 | cc := cmdcontext.NewAutoCloseContext(ctx, worker, ticket.GuildId, *ticket.ChannelId, request.UserId, premiumTier) 57 | logic.CloseTicket(ctx, cc, request.Reason, true) 58 | }() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /bot/listeners/messagequeue/contextbuilder.go: -------------------------------------------------------------------------------- 1 | package messagequeue 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/database" 6 | "github.com/TicketsBot/worker" 7 | "github.com/TicketsBot/worker/bot/dbclient" 8 | "github.com/TicketsBot/worker/config" 9 | "github.com/rxdn/gdl/cache" 10 | ) 11 | 12 | func buildContext(ctx context.Context, ticket database.Ticket, cache *cache.PgCache) (*worker.Context, error) { 13 | worker := &worker.Context{ 14 | Cache: cache, 15 | RateLimiter: nil, // Use http-proxy ratelimiting functionality 16 | } 17 | 18 | whitelabelBotId, isWhitelabel, err := dbclient.Client.WhitelabelGuilds.GetBotByGuild(ctx, ticket.GuildId) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | worker.IsWhitelabel = isWhitelabel 24 | 25 | if isWhitelabel { 26 | res, err := dbclient.Client.Whitelabel.GetByBotId(ctx, whitelabelBotId) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | worker.Token = res.Token 32 | worker.BotId = whitelabelBotId 33 | } else { 34 | worker.Token = config.Conf.Discord.Token 35 | worker.BotId = config.Conf.Discord.PublicBotId 36 | } 37 | 38 | return worker, err 39 | } 40 | -------------------------------------------------------------------------------- /bot/listeners/register.go: -------------------------------------------------------------------------------- 1 | package listeners 2 | 3 | func init() { 4 | ChannelDeleteListeners = append(ChannelDeleteListeners, OnChannelDelete) 5 | GuildCreateListeners = append(GuildCreateListeners, OnGuildCreate) 6 | GuildDeleteListeners = append(GuildDeleteListeners, OnGuildLeave) 7 | GuildMemberRemoveListeners = append(GuildMemberRemoveListeners, OnMemberLeave) 8 | GuildMemberUpdateListeners = append(GuildMemberUpdateListeners, OnMemberUpdate) 9 | MessageCreateListeners = append(MessageCreateListeners, OnMessage) 10 | GuildRoleDeleteListeners = append(GuildRoleDeleteListeners, OnRoleDelete) 11 | ThreadMembersUpdateListeners = append(ThreadMembersUpdateListeners, OnThreadMembersUpdate) 12 | ThreadUpdateListeners = append(ThreadUpdateListeners, OnThreadUpdate) 13 | } 14 | -------------------------------------------------------------------------------- /bot/listeners/roledelete.go: -------------------------------------------------------------------------------- 1 | package listeners 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/sentry" 6 | "github.com/TicketsBot/worker" 7 | "github.com/TicketsBot/worker/bot/dbclient" 8 | "github.com/TicketsBot/worker/bot/errorcontext" 9 | "github.com/rxdn/gdl/gateway/payloads/events" 10 | "golang.org/x/sync/errgroup" 11 | "time" 12 | ) 13 | 14 | func OnRoleDelete(worker *worker.Context, e events.GuildRoleDelete) { 15 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) // TODO: Propagate context 16 | defer cancel() 17 | 18 | errorCtx := errorcontext.WorkerErrorContext{Guild: e.GuildId} 19 | 20 | group, _ := errgroup.WithContext(context.Background()) 21 | 22 | group.Go(func() error { 23 | return dbclient.Client.RolePermissions.RemoveSupport(ctx, e.GuildId, e.RoleId) 24 | }) 25 | 26 | group.Go(func() error { 27 | return dbclient.Client.SupportTeamRoles.DeleteAllRole(ctx, e.RoleId) 28 | }) 29 | 30 | group.Go(func() error { 31 | return dbclient.Client.PanelRoleMentions.DeleteAllRole(ctx, e.RoleId) 32 | }) 33 | 34 | if err := group.Wait(); err != nil { 35 | sentry.ErrorWithContext(err, errorCtx) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bot/listeners/threadmembersupdate.go: -------------------------------------------------------------------------------- 1 | package listeners 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/sentry" 6 | "github.com/TicketsBot/database" 7 | "github.com/TicketsBot/worker" 8 | "github.com/TicketsBot/worker/bot/dbclient" 9 | "github.com/TicketsBot/worker/bot/errorcontext" 10 | "github.com/TicketsBot/worker/bot/logic" 11 | "github.com/TicketsBot/worker/bot/utils" 12 | "github.com/rxdn/gdl/gateway/payloads/events" 13 | "time" 14 | ) 15 | 16 | func OnThreadMembersUpdate(worker *worker.Context, e events.ThreadMembersUpdate) { 17 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*6) // TODO: Propagate context 18 | defer cancel() 19 | 20 | settings, err := dbclient.Client.Settings.Get(ctx, e.GuildId) 21 | if err != nil { 22 | sentry.ErrorWithContext(err, errorcontext.WorkerErrorContext{Guild: e.GuildId}) 23 | return 24 | } 25 | 26 | ticket, err := dbclient.Client.Tickets.GetByChannelAndGuild(ctx, e.ThreadId, e.GuildId) 27 | if err != nil { 28 | sentry.ErrorWithContext(err, errorcontext.WorkerErrorContext{Guild: e.GuildId}) 29 | return 30 | } 31 | 32 | if ticket.Id == 0 || ticket.GuildId != e.GuildId { 33 | return 34 | } 35 | 36 | if ticket.JoinMessageId != nil { 37 | var panel *database.Panel 38 | if ticket.PanelId != nil { 39 | tmp, err := dbclient.Client.Panel.GetById(ctx, *ticket.PanelId) 40 | if err != nil { 41 | sentry.ErrorWithContext(err, errorcontext.WorkerErrorContext{Guild: e.GuildId}) 42 | return 43 | } 44 | 45 | if tmp.PanelId != 0 && e.GuildId == tmp.GuildId { 46 | panel = &tmp 47 | } 48 | } 49 | 50 | premiumTier, err := utils.PremiumClient.GetTierByGuildId(ctx, e.GuildId, true, worker.Token, worker.RateLimiter) 51 | if err != nil { 52 | sentry.ErrorWithContext(err, errorcontext.WorkerErrorContext{Guild: e.GuildId}) 53 | return 54 | } 55 | 56 | threadStaff, err := logic.GetStaffInThread(ctx, worker, ticket, e.ThreadId) 57 | if err != nil { 58 | sentry.ErrorWithContext(err, errorcontext.WorkerErrorContext{Guild: e.GuildId}) 59 | return 60 | } 61 | 62 | if settings.TicketNotificationChannel != nil { 63 | data := logic.BuildJoinThreadMessage(ctx, worker, ticket.GuildId, ticket.UserId, ticket.Id, panel, threadStaff, premiumTier) 64 | if _, err := worker.EditMessage(*settings.TicketNotificationChannel, *ticket.JoinMessageId, data.IntoEditMessageData()); err != nil { 65 | sentry.ErrorWithContext(err, errorcontext.WorkerErrorContext{Guild: e.GuildId}) 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /bot/logic/substitutions.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/worker/bot/command/registry" 6 | "github.com/rxdn/gdl/objects/member" 7 | "github.com/rxdn/gdl/objects/user" 8 | "strings" 9 | ) 10 | 11 | type SubstitutionFunc func(user user.User, member member.Member) string 12 | 13 | type Substitutor struct { 14 | Placeholder string 15 | NeedsUser bool 16 | NeedsMember bool 17 | F SubstitutionFunc 18 | } 19 | 20 | func NewSubstitutor(placeholder string, needsUser, needsMember bool, f SubstitutionFunc) Substitutor { 21 | return Substitutor{ 22 | Placeholder: placeholder, 23 | NeedsUser: needsUser, 24 | NeedsMember: needsMember, 25 | F: f, 26 | } 27 | } 28 | 29 | func doSubstitutions(ctx registry.CommandContext, s string, userId uint64, substitutors []Substitutor) (string, error) { 30 | var needsUser, needsMember bool 31 | 32 | // Determine which objects we need to fetch 33 | for _, substitutor := range substitutors { 34 | if substitutor.NeedsUser { 35 | needsUser = true 36 | } 37 | 38 | if substitutor.NeedsMember { 39 | needsMember = true 40 | } 41 | 42 | if needsUser && needsMember { 43 | break 44 | } 45 | } 46 | 47 | // Retrieve user and member if necessary 48 | var user user.User 49 | var member member.Member 50 | 51 | var err error 52 | if needsUser { 53 | if ctx.UserId() == userId { 54 | user, err = ctx.User() 55 | } else { 56 | user, err = ctx.Worker().GetUser(userId) 57 | } 58 | } 59 | 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | if needsMember { 65 | if ctx.UserId() == userId { 66 | member, err = ctx.Member() 67 | } else { 68 | member, err = ctx.Worker().GetGuildMember(ctx.GuildId(), userId) 69 | } 70 | } 71 | 72 | if err != nil { 73 | return "", err 74 | } 75 | 76 | for _, substitutor := range substitutors { 77 | placeholder := fmt.Sprintf("%%%s%%", substitutor.Placeholder) 78 | 79 | if strings.Contains(s, placeholder) { 80 | s = strings.ReplaceAll(s, placeholder, substitutor.F(user, member)) 81 | } 82 | } 83 | 84 | return s, nil 85 | } 86 | -------------------------------------------------------------------------------- /bot/metrics/prometheus/hooks.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/rxdn/gdl/rest/request" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | func PreRequestHook(_ string, req *http.Request) { 13 | ActiveHttpRequests.Inc() 14 | 15 | ctx := context.WithValue(req.Context(), "rt", time.Now()) 16 | *req = *req.WithContext(ctx) 17 | } 18 | 19 | func PostRequestHook(res *http.Response, body []byte) { 20 | ActiveHttpRequests.Dec() 21 | 22 | if res == nil { 23 | return 24 | } 25 | 26 | if requestTime := res.Request.Context().Value("rt"); requestTime != nil { 27 | duration := time.Since(requestTime.(time.Time)) 28 | HttpRequestDuration.Observe(duration.Seconds()) 29 | } 30 | 31 | if res.StatusCode >= 400 { 32 | var apiError request.ApiV8Error 33 | if err := json.Unmarshal(body, &apiError); err != nil { 34 | DiscordApiErrors.WithLabelValues(strconv.Itoa(res.StatusCode), "UNKNOWN").Inc() 35 | return 36 | } 37 | 38 | DiscordApiErrors.WithLabelValues(strconv.Itoa(res.StatusCode), apiError.Message).Inc() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /bot/metrics/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "github.com/TicketsBot/database" 5 | "github.com/prometheus/client_golang/prometheus" 6 | "github.com/prometheus/client_golang/prometheus/promauto" 7 | "strconv" 8 | ) 9 | 10 | const ( 11 | Namespace = "tickets" 12 | Subsystem = "worker" 13 | ) 14 | 15 | var ( 16 | IntegrationRequests = newCounterVec("integration_requests", "integration_id", "integration_name", "guild_id") 17 | TicketsCreated = newCounter("tickets_created") 18 | 19 | Commands = newCounterVec("commands", "command") 20 | 21 | InteractionTimeToDefer = newHistogram("interaction_time_to_defer") 22 | InteractionTimeToReceive = newHistogram("interaction_time_to_receive") 23 | 24 | OnMessageTicketLookup = newCounterVec("on_message_ticket_lookup_count", "is_ticket", "cache_hit") 25 | 26 | ActiveHttpRequests = newGauge("active_http_requests") 27 | HttpRequestDuration = newHistogram("http_request_duration") 28 | DiscordApiErrors = newCounterVec("discord_api_errors", "status", "error_code") 29 | 30 | InboundRequests = newCounterVec("inbound_requests", "route") 31 | ActiveInteractions = newGauge("active_interactions") 32 | InteractionTimeToComplete = newHistogram("interaction_time_to_complete") 33 | 34 | ForwardedDashboardMessages = newCounter("forwarded_dashboard_messages") 35 | 36 | Events = newCounterVec("events", "event_type") 37 | KafkaBatchSize = newHistogram("kafka_batch_size") 38 | KafkaMessages = newHistogramVec("kafka_messages", "topic") 39 | 40 | CategoryUpdates = newCounter("category_updates") 41 | ) 42 | 43 | func newCounter(name string) prometheus.Counter { 44 | return promauto.NewCounter(prometheus.CounterOpts{ 45 | Namespace: Namespace, 46 | Subsystem: Subsystem, 47 | Name: name, 48 | }) 49 | } 50 | 51 | func newCounterVec(name string, labels ...string) *prometheus.CounterVec { 52 | return promauto.NewCounterVec(prometheus.CounterOpts{ 53 | Namespace: Namespace, 54 | Subsystem: Subsystem, 55 | Name: name, 56 | }, labels) 57 | } 58 | 59 | func newHistogram(name string) prometheus.Histogram { 60 | return promauto.NewHistogram(prometheus.HistogramOpts{ 61 | Namespace: Namespace, 62 | Subsystem: Subsystem, 63 | Name: name, 64 | }) 65 | } 66 | 67 | func newHistogramVec(name string, labels ...string) *prometheus.HistogramVec { 68 | return promauto.NewHistogramVec(prometheus.HistogramOpts{ 69 | Namespace: Namespace, 70 | Subsystem: Subsystem, 71 | Name: name, 72 | }, labels) 73 | } 74 | 75 | func newGauge(name string) prometheus.Gauge { 76 | return promauto.NewGauge(prometheus.GaugeOpts{ 77 | Namespace: Namespace, 78 | Subsystem: Subsystem, 79 | Name: name, 80 | }) 81 | } 82 | 83 | func LogIntegrationRequest(integration database.CustomIntegration, guildId uint64) { 84 | IntegrationRequests.WithLabelValues( 85 | strconv.Itoa(integration.Id), 86 | integration.Name, 87 | strconv.FormatUint(guildId, 10), 88 | ).Inc() 89 | } 90 | 91 | func LogCommand(command string) { 92 | Commands.WithLabelValues(command).Inc() 93 | } 94 | 95 | func LogOnMessageTicketLookup(isTicket, cacheHit bool) { 96 | OnMessageTicketLookup.WithLabelValues(strconv.FormatBool(isTicket), strconv.FormatBool(cacheHit)).Inc() 97 | } 98 | -------------------------------------------------------------------------------- /bot/metrics/prometheus/server.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "fmt" 5 | "github.com/prometheus/client_golang/prometheus/promhttp" 6 | "net/http" 7 | ) 8 | 9 | func StartServer(serverAddr string) { 10 | mux := http.NewServeMux() 11 | mux.Handle("/metrics", promhttp.Handler()) 12 | 13 | go func() { 14 | if err := http.ListenAndServe(serverAddr, mux); err != nil { 15 | fmt.Printf("Error starting prometheus server: %s\n", err.Error()) 16 | } 17 | }() 18 | } 19 | -------------------------------------------------------------------------------- /bot/metrics/statsd/keys.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | type Key string 4 | 5 | const ( 6 | KeyMessages Key = "messages" 7 | KeyTickets Key = "tickets" 8 | KeyCommands Key = "commands" 9 | KeyJoins Key = "joins" 10 | KeyLeaves Key = "leaves" 11 | KeyRest Key = "rest" 12 | KeySlashCommands Key = "slash_commands" 13 | KeyEvents Key = "events" 14 | AutoClose Key = "autoclose" 15 | KeyDirectMessage Key = "direct_message" 16 | KeyOpenCommand Key = "open_command" 17 | ) 18 | 19 | func (k Key) String() string { 20 | return string(k) 21 | } 22 | 23 | func AllKeys() []Key { 24 | return []Key{ 25 | KeyMessages, 26 | KeyTickets, 27 | KeyCommands, 28 | KeyJoins, 29 | KeyLeaves, 30 | KeyRest, 31 | KeySlashCommands, 32 | KeyEvents, 33 | AutoClose, 34 | KeyDirectMessage, 35 | KeyOpenCommand, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bot/metrics/statsd/resthook.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | import "net/http" 4 | 5 | func RestHook(string, *http.Request) { 6 | Client.IncrementKey(KeyRest) 7 | } 8 | -------------------------------------------------------------------------------- /bot/metrics/statsd/statsd.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | import ( 4 | "go.uber.org/atomic" 5 | stats "gopkg.in/alexcesaro/statsd.v2" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type StatsdClient struct { 11 | client *stats.Client 12 | buffer map[Key]*atomic.Int32 13 | mu *sync.Mutex 14 | } 15 | 16 | var Client StatsdClient 17 | 18 | func NewClient(statsdAddress, statsdPrefix string) (StatsdClient, error) { 19 | client, err := stats.New(stats.Address(statsdAddress), stats.Prefix(statsdPrefix)) 20 | if err != nil { 21 | return StatsdClient{}, err 22 | } 23 | 24 | buffer := make(map[Key]*atomic.Int32) 25 | for _, key := range AllKeys() { 26 | buffer[key] = atomic.NewInt32(0) 27 | } 28 | 29 | return StatsdClient{ 30 | client: client, 31 | buffer: buffer, 32 | mu: &sync.Mutex{}, 33 | }, nil 34 | } 35 | 36 | func (c *StatsdClient) StartDaemon() { 37 | ticker := time.NewTicker(time.Second * 15) 38 | defer ticker.Stop() 39 | 40 | for { 41 | select { 42 | case _ = <-ticker.C: 43 | for key, count := range c.buffer { 44 | c.client.Count(key.String(), count.Swap(0)) 45 | } 46 | } 47 | } 48 | } 49 | 50 | func (c *StatsdClient) IncrementKey(key Key) { 51 | if c.buffer[key] == nil { 52 | return 53 | } 54 | 55 | c.buffer[key].Inc() 56 | } 57 | -------------------------------------------------------------------------------- /bot/redis/channelrefetchtimer.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const channelRefetchBackoff = time.Minute * 5 10 | 11 | func TakeChannelRefetchToken(ctx context.Context, guildId uint64) (bool, error) { 12 | key := fmt.Sprintf("channelrefetch:%d", guildId) 13 | return Client.SetNX(ctx, key, 1, channelRefetchBackoff).Result() 14 | } 15 | -------------------------------------------------------------------------------- /bot/redis/client.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/TicketsBot/worker/config" 5 | "github.com/go-redis/redis/v8" 6 | "github.com/go-redsync/redsync/v4" 7 | redsyncredis "github.com/go-redsync/redsync/v4/redis/goredis/v8" 8 | ) 9 | 10 | var ( 11 | Client *redis.Client 12 | rs *redsync.Redsync 13 | ) 14 | 15 | var ErrNil = redis.Nil 16 | 17 | func Connect() error { 18 | Client = redis.NewClient(&redis.Options{ 19 | Network: "tcp", 20 | Addr: config.Conf.Redis.Address, 21 | Password: config.Conf.Redis.Password, 22 | PoolSize: config.Conf.Redis.Threads, 23 | MinIdleConns: config.Conf.Redis.Threads, 24 | }) 25 | 26 | pool := redsyncredis.NewPool(Client) 27 | rs = redsync.New(pool) 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /bot/redis/commandids.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | func LoadCommandIds(botId uint64) (map[string]uint64, error) { 11 | data, err := Client.HGetAll(context.Background(), buildCommandIdKey(botId)).Result() 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | parsed := make(map[string]uint64) 17 | for name, idRaw := range data { 18 | id, err := strconv.ParseUint(idRaw, 10, 64) 19 | if err != nil { 20 | return nil, fmt.Errorf("error parsing id %s for command %s for bot %d", idRaw, name, botId) 21 | } 22 | 23 | parsed[name] = id 24 | } 25 | 26 | return parsed, nil 27 | } 28 | 29 | func StoreCommandIds(botId uint64, commandIds map[string]uint64) error { 30 | key := buildCommandIdKey(botId) 31 | 32 | mapped := make(map[string]interface{}) 33 | for name, id := range commandIds { 34 | mapped[name] = id 35 | } 36 | 37 | tx := Client.TxPipeline() 38 | tx.HSet(context.Background(), key, mapped) 39 | tx.Expire(context.Background(), key, time.Minute*5) 40 | 41 | _, err := tx.Exec(context.Background()) 42 | return err 43 | } 44 | 45 | func buildCommandIdKey(botId uint64) string { 46 | return fmt.Sprintf("commandsids:%d", botId) 47 | } 48 | -------------------------------------------------------------------------------- /bot/redis/dmchannelcache.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/TicketsBot/common/utils" 7 | "github.com/go-redis/redis/v8" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | var ErrNotCached = errors.New("channel not cached") 13 | 14 | // Returns nil if we cannot create a channel 15 | // Returns ErrNotCached if not cached 16 | func GetDMChannel(userId, botId uint64) (*uint64, error) { 17 | key := fmt.Sprintf("dmchannel:%d:%d", botId, userId) 18 | 19 | res, err := Client.Get(utils.DefaultContext(), key).Result() 20 | if err != nil { 21 | if err == redis.Nil { 22 | return nil, ErrNotCached 23 | } 24 | 25 | return nil, err 26 | } 27 | 28 | if res == "null" { 29 | return nil, nil 30 | } 31 | 32 | parsed, err := strconv.ParseUint(res, 10, 64) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return &parsed, nil 38 | } 39 | 40 | func StoreNullDMChannel(userId, botId uint64) error { 41 | key := fmt.Sprintf("dmchannel:%d:%d", botId, userId) 42 | return Client.Set(utils.DefaultContext(), key, "null", time.Hour * 6).Err() 43 | } 44 | 45 | func StoreDMChannel(userId, channelId, botId uint64) error { 46 | key := fmt.Sprintf("dmchannel:%d:%d", botId, userId) 47 | return Client.Set(utils.DefaultContext(), key, strconv.FormatUint(channelId, 10), 0).Err() 48 | } 49 | -------------------------------------------------------------------------------- /bot/redis/integrationrolecache.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | const IntegrationRoleCacheExpiry = time.Minute * 5 12 | 13 | var ErrIntegrationRoleNotCached = errors.New("integration role not cached") 14 | 15 | func GetIntegrationRole(ctx context.Context, guildId, botId uint64) (uint64, error) { 16 | key := fmt.Sprintf("integrationrole:%d:%d", guildId, botId) 17 | res, err := Client.Get(ctx, key).Result() 18 | if err != nil { 19 | if errors.Is(err, ErrNil) { 20 | return 0, ErrIntegrationRoleNotCached 21 | } 22 | 23 | return 0, err 24 | } 25 | 26 | roleId, err := strconv.ParseUint(res, 10, 64) 27 | if err != nil { 28 | return 0, err 29 | } 30 | 31 | return roleId, nil 32 | } 33 | 34 | func SetIntegrationRole(ctx context.Context, guildId, botId, roleId uint64) error { 35 | key := fmt.Sprintf("integrationrole:%d:%d", guildId, botId) 36 | return Client.Set(ctx, key, roleId, IntegrationRoleCacheExpiry).Err() 37 | } 38 | 39 | func DeleteIntegrationRole(ctx context.Context, guildId, botId uint64) error { 40 | key := fmt.Sprintf("integrationrole:%d:%d", guildId, botId) 41 | return Client.Del(ctx, key).Err() 42 | } 43 | -------------------------------------------------------------------------------- /bot/redis/openlock.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/go-redsync/redsync/v4" 7 | "time" 8 | ) 9 | 10 | type Mutex interface { 11 | LockContext(ctx context.Context) error 12 | UnlockContext(ctx context.Context) (bool, error) 13 | } 14 | 15 | const TicketOpenLockExpiry = time.Second * 3 16 | 17 | var ErrLockExpired = redsync.ErrLockAlreadyExpired 18 | 19 | func TakeTicketOpenLock(ctx context.Context, guildId uint64) (Mutex, error) { 20 | mu := rs.NewMutex(fmt.Sprintf("tickets:openlock:%d", guildId), redsync.WithExpiry(TicketOpenLockExpiry)) 21 | if err := mu.LockContext(ctx); err != nil { 22 | return nil, err 23 | } 24 | 25 | return mu, nil 26 | } 27 | -------------------------------------------------------------------------------- /bot/redis/renameratelimit.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const ( 10 | renameRatelimitExpiry = time.Minute * 10 11 | renameRatelimitTokens = 2 12 | ) 13 | 14 | func TakeRenameRatelimit(ctx context.Context, channelId uint64) (bool, error) { 15 | key := fmt.Sprintf("tickets:rename_ratelimit:%d", channelId) 16 | 17 | tx := Client.TxPipeline() 18 | tx.SetNX(ctx, key, "0", renameRatelimitExpiry) 19 | incr := tx.Incr(ctx, key) 20 | 21 | if _, err := tx.Exec(ctx); err != nil { 22 | return false, err 23 | } 24 | 25 | count, err := incr.Result() 26 | if err != nil { 27 | return false, err 28 | } 29 | 30 | return count <= renameRatelimitTokens, nil 31 | } 32 | -------------------------------------------------------------------------------- /bot/redis/ticketchannelcache.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | const TicketStatusCacheExpiry = time.Second * 90 11 | 12 | var ErrTicketStatusNotCached = errors.New("ticket status not cached") 13 | 14 | func IsTicketChannel(ctx context.Context, channelId uint64) (bool, error) { 15 | key := fmt.Sprintf("isticket:%d", channelId) 16 | res, err := Client.Get(ctx, key).Result() 17 | if err != nil { 18 | if errors.Is(err, ErrNil) { 19 | return false, ErrTicketStatusNotCached 20 | } 21 | 22 | return false, err 23 | } 24 | 25 | return res == "1", nil 26 | } 27 | 28 | func SetTicketChannelStatus(ctx context.Context, channelId uint64, isTicket bool) error { 29 | key := fmt.Sprintf("isticket:%d", channelId) 30 | 31 | var value string 32 | if isTicket { 33 | value = "1" 34 | } else { 35 | value = "0" 36 | } 37 | 38 | return Client.Set(ctx, key, value, TicketStatusCacheExpiry).Err() 39 | } 40 | -------------------------------------------------------------------------------- /bot/redis/ticketratelimit.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TicketsBot/common/utils" 6 | "github.com/go-redis/redis/v8" 7 | "time" 8 | ) 9 | 10 | var script = redis.NewScript(` 11 | local current = redis.call("GET", KEYS[1]) 12 | local notExists = (not current) 13 | 14 | if not current then 15 | current = 0 16 | else 17 | current = tonumber(current) 18 | end 19 | 20 | local success = 0 21 | if current < tonumber(ARGV[1]) then 22 | redis.call("INCR", KEYS[1]) 23 | 24 | if notExists then 25 | redis.call("EXPIRE", KEYS[1], ARGV[2]) 26 | end 27 | 28 | success = 1 29 | end 30 | 31 | return success 32 | `) 33 | 34 | var TicketOpenLimit = 10 35 | var TicketOpenLimitInterval = time.Second * 30 36 | 37 | func TakeTicketRateLimitToken(client *redis.Client, guildId uint64) (bool, error) { 38 | key := fmt.Sprintf("tickets:openratelimit:%d", guildId) 39 | 40 | res, err := script.Run(utils.DefaultContext(), client, []string{key}, TicketOpenLimit, TicketOpenLimitInterval.Seconds()).Result() 41 | if err != nil { 42 | return false, err 43 | } 44 | 45 | i, ok := res.(int64) 46 | if !ok { 47 | return false, fmt.Errorf("ratelimit token returned %v, not an int64", res) 48 | } 49 | 50 | return i == 1, nil 51 | } 52 | -------------------------------------------------------------------------------- /bot/rpc/listeners/base.go: -------------------------------------------------------------------------------- 1 | package listeners 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/TicketsBot/worker" 7 | "github.com/TicketsBot/worker/bot/dbclient" 8 | "github.com/TicketsBot/worker/config" 9 | "github.com/rxdn/gdl/cache" 10 | "time" 11 | ) 12 | 13 | type BaseListener struct { 14 | cache *cache.PgCache 15 | } 16 | 17 | const Timeout = time.Second * 15 18 | 19 | func NewBaseListener(cache *cache.PgCache) *BaseListener { 20 | return &BaseListener{ 21 | cache: cache, 22 | } 23 | } 24 | 25 | func (b *BaseListener) BuildContext() (context.Context, context.CancelFunc) { 26 | return context.WithTimeout(context.Background(), Timeout) 27 | } 28 | 29 | func (b *BaseListener) ContextForGuild(ctx context.Context, guildId uint64) (*worker.Context, error) { 30 | botId, isWhitelabel, err := dbclient.Client.WhitelabelGuilds.GetBotByGuild(ctx, guildId) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if isWhitelabel { 36 | // TODO: Merge lookup into one query 37 | bot, err := dbclient.Client.Whitelabel.GetByBotId(ctx, botId) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | if bot.BotId == 0 { 43 | return nil, errors.New("bot not found") 44 | } 45 | 46 | return &worker.Context{ 47 | Token: bot.Token, 48 | BotId: bot.BotId, 49 | IsWhitelabel: true, 50 | ShardId: 0, 51 | Cache: b.cache, 52 | RateLimiter: nil, 53 | }, nil 54 | } else { 55 | return &worker.Context{ 56 | Token: config.Conf.Discord.Token, 57 | BotId: config.Conf.Discord.PublicBotId, 58 | IsWhitelabel: false, 59 | ShardId: 0, 60 | Cache: b.cache, 61 | RateLimiter: nil, 62 | }, nil 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /bot/utils/adminutils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/TicketsBot/worker/config" 5 | ) 6 | 7 | func IsBotAdmin(id uint64) bool { 8 | for _, admin := range config.Conf.Bot.Admins { 9 | if admin == id { 10 | return true 11 | } 12 | } 13 | 14 | return false 15 | } 16 | 17 | func IsBotHelper(id uint64) bool { 18 | if IsBotAdmin(id) { 19 | return true 20 | } 21 | 22 | for _, helper := range config.Conf.Bot.Helpers { 23 | if helper == id { 24 | return true 25 | } 26 | } 27 | 28 | return false 29 | } 30 | -------------------------------------------------------------------------------- /bot/utils/archive.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/TicketsBot/archiverclient" 4 | 5 | var ArchiverClient *archiverclient.ArchiverClient 6 | -------------------------------------------------------------------------------- /bot/utils/blacklist.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/worker/bot/blacklist" 7 | "github.com/TicketsBot/worker/bot/dbclient" 8 | "github.com/rxdn/gdl/objects/member" 9 | "golang.org/x/sync/errgroup" 10 | ) 11 | 12 | // Get whether the user is blacklisted at either global or server level 13 | func IsBlacklisted(ctx context.Context, guildId, userId uint64, member member.Member, permLevel permission.PermissionLevel) (bool, error) { 14 | if blacklist.IsUserBlacklisted(userId) { 15 | return true, nil 16 | } 17 | 18 | var userBlacklisted, roleBlacklisted bool 19 | 20 | group, _ := errgroup.WithContext(ctx) 21 | group.Go(func() error { 22 | tmp, err := dbclient.Client.Blacklist.IsBlacklisted(ctx, guildId, userId) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | if tmp { 28 | userBlacklisted = true 29 | } 30 | 31 | return nil 32 | }) 33 | 34 | group.Go(func() error { 35 | tmp, err := dbclient.Client.RoleBlacklist.IsAnyBlacklisted(ctx, guildId, member.Roles) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | if tmp { 41 | roleBlacklisted = true 42 | } 43 | 44 | return nil 45 | }) 46 | 47 | if err := group.Wait(); err != nil { 48 | return false, err 49 | } 50 | 51 | // Have staff override role blacklist 52 | return permLevel < permission.Support && (userBlacklisted || roleBlacklisted), nil 53 | } 54 | -------------------------------------------------------------------------------- /bot/utils/context.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | func ContextTimeout(timeout time.Duration) (context.Context, context.CancelFunc) { 9 | return context.WithTimeout(context.Background(), timeout) 10 | } 11 | -------------------------------------------------------------------------------- /bot/utils/conversion.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func Ptr[T any](t T) *T { 4 | return &t 5 | } 6 | 7 | func PtrElems[T any](t []T) []*T { 8 | arr := make([]*T, len(t)) 9 | for i, v := range t { 10 | arr[i] = &v 11 | } 12 | 13 | return arr 14 | } 15 | 16 | func Slice[T any](v ...T) []T { 17 | return v 18 | } 19 | 20 | func ValueOrZero[T any](v *T) T { 21 | if v == nil { 22 | return *new(T) 23 | } else { 24 | return *v 25 | } 26 | } 27 | 28 | func ValueOrDefault[T any](v *T, def T) T { 29 | if v == nil { 30 | return def 31 | } else { 32 | return *v 33 | } 34 | } 35 | 36 | func NilIfZero[T comparable](v T) *T { 37 | if v == *new(T) { 38 | return nil 39 | } else { 40 | return &v 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /bot/utils/discordutils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | const DiscordEpoch uint64 = 1420070400000 9 | 10 | func SnowflakeToTime(snowflake uint64) time.Time { 11 | return time.UnixMilli(int64((snowflake >> 22) + DiscordEpoch)) 12 | } 13 | 14 | func EscapeMarkdown(s string) string { 15 | var builder strings.Builder 16 | var inLink bool 17 | 18 | builder.Grow(len(s)) 19 | 20 | for i, c := range s { 21 | if c == ' ' { 22 | inLink = false 23 | } 24 | 25 | if !inLink { 26 | if c == 'h' || c == 'H' { 27 | if len(s) >= i+8 && strings.EqualFold(s[i:i+8], "https://") { 28 | inLink = true 29 | } else if len(s) >= i+7 && strings.EqualFold(s[i:i+7], "http://") { 30 | inLink = true 31 | } 32 | } 33 | 34 | if c == '*' || c == '_' || c == '`' || c == '~' || c == '|' || c == '#' { 35 | builder.WriteRune('\\') 36 | } 37 | } 38 | 39 | builder.WriteRune(c) 40 | } 41 | 42 | return builder.String() 43 | } 44 | -------------------------------------------------------------------------------- /bot/utils/errorutils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/TicketsBot/worker/bot/errorcontext" 5 | "github.com/rxdn/gdl/gateway/payloads/events" 6 | ) 7 | 8 | func MessageCreateErrorContext(e events.MessageCreate) errorcontext.WorkerErrorContext { 9 | return errorcontext.WorkerErrorContext{ 10 | Guild: e.GuildId, 11 | User: e.Author.Id, 12 | Channel: e.ChannelId, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /bot/utils/hostname.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "os" 4 | 5 | const DefaultServiceName = "worker" 6 | 7 | func GetServiceName() string { 8 | if _, ok := os.LookupEnv("KUBERNETES_SERVICE_HOST"); !ok { 9 | return DefaultServiceName 10 | } 11 | 12 | hostname, err := os.Hostname() 13 | if err != nil || len(hostname) == 0 { 14 | return DefaultServiceName 15 | } else { 16 | var dashCount int 17 | 18 | for i := len(hostname) - 1; i >= 0; i-- { 19 | if hostname[i] == '-' { 20 | dashCount++ 21 | } 22 | 23 | if dashCount == 2 { 24 | return hostname[:i] 25 | } 26 | } 27 | 28 | return DefaultServiceName 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bot/utils/interactionutils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/rxdn/gdl/objects/interaction" 6 | "strconv" 7 | ) 8 | 9 | func ButtonInteractionUser(data interaction.MessageComponentInteraction) uint64 { 10 | if data.User != nil { 11 | return data.User.Id 12 | } else if data.Member != nil { 13 | return data.Member.User.Id 14 | } else { // Impossible 15 | return 0 16 | } 17 | } 18 | 19 | func StringChoice(value string) interaction.ApplicationCommandOptionChoice { 20 | return interaction.ApplicationCommandOptionChoice{ 21 | Name: value, 22 | Value: value, 23 | } 24 | } 25 | 26 | func IntChoice(value int) interaction.ApplicationCommandOptionChoice { 27 | return interaction.ApplicationCommandOptionChoice{ 28 | Name: strconv.Itoa(value), 29 | Value: value, 30 | } 31 | } 32 | 33 | func FloatChoice(value float32) interaction.ApplicationCommandOptionChoice { 34 | return interaction.ApplicationCommandOptionChoice{ 35 | Name: fmt.Sprintf("%f", value), 36 | Value: value, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bot/utils/markdown_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "testing" 6 | ) 7 | 8 | func TestNoEscape(t *testing.T) { 9 | input := "hello world" 10 | require.Equal(t, input, EscapeMarkdown(input)) 11 | } 12 | 13 | func TestEscapeBold(t *testing.T) { 14 | input := "hello **world**" 15 | require.Equal(t, "hello \\*\\*world\\*\\*", EscapeMarkdown(input)) 16 | } 17 | 18 | func TestEscapeMulti(t *testing.T) { 19 | input := "hello __**world**__" 20 | require.Equal(t, "hello \\_\\_\\*\\*world\\*\\*\\_\\_", EscapeMarkdown(input)) 21 | } 22 | 23 | func TestEscapeLink(t *testing.T) { 24 | input := "hello https://google.com/some_path_here **hello world**" 25 | expected := "hello https://google.com/some_path_here \\*\\*hello world\\*\\*" 26 | require.Equal(t, expected, EscapeMarkdown(input)) 27 | } 28 | 29 | func TestHttpsIncomplete(t *testing.T) { 30 | input := "hello https:/" 31 | require.Equal(t, input, EscapeMarkdown(input)) 32 | } 33 | 34 | func TestHttpIncomplete(t *testing.T) { 35 | input := "hello http:/" 36 | require.Equal(t, input, EscapeMarkdown(input)) 37 | } 38 | -------------------------------------------------------------------------------- /bot/utils/messageutils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/premium" 6 | "github.com/TicketsBot/worker" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/customisation" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/i18n" 11 | "github.com/rxdn/gdl/objects/channel/embed" 12 | "github.com/rxdn/gdl/objects/guild/emoji" 13 | ) 14 | 15 | func BuildEmbed( 16 | ctx registry.CommandContext, 17 | colour customisation.Colour, titleId, contentId i18n.MessageId, fields []embed.EmbedField, 18 | format ...interface{}, 19 | ) *embed.Embed { 20 | title := i18n.GetMessageFromGuild(ctx.GuildId(), titleId) 21 | content := i18n.GetMessageFromGuild(ctx.GuildId(), contentId, format...) 22 | 23 | msgEmbed := embed.NewEmbed(). 24 | SetColor(ctx.GetColour(colour)). 25 | SetTitle(title). 26 | SetDescription(content) 27 | 28 | for _, field := range fields { 29 | msgEmbed.AddField(field.Name, field.Value, field.Inline) 30 | } 31 | 32 | if ctx.PremiumTier() == premium.None { 33 | msgEmbed.SetFooter("Powered by ticketsbot.net", "https://ticketsbot.net/assets/img/logo.png") 34 | } 35 | 36 | return msgEmbed 37 | } 38 | 39 | func BuildEmbedRaw( 40 | colourHex int, title, content string, fields []embed.EmbedField, tier premium.PremiumTier, 41 | ) *embed.Embed { 42 | msgEmbed := embed.NewEmbed(). 43 | SetColor(colourHex). 44 | SetTitle(title). 45 | SetDescription(content) 46 | 47 | for _, field := range fields { 48 | msgEmbed.AddField(field.Name, field.Value, field.Inline) 49 | } 50 | 51 | if tier == premium.None { 52 | msgEmbed.SetFooter("Powered by ticketsbot.net", "https://ticketsbot.net/assets/img/logo.png") 53 | } 54 | 55 | return msgEmbed 56 | } 57 | 58 | func GetColourForGuild(ctx context.Context, worker *worker.Context, colour customisation.Colour, guildId uint64) (int, error) { 59 | premiumTier, err := PremiumClient.GetTierByGuildId(ctx, guildId, true, worker.Token, worker.RateLimiter) 60 | if err != nil { 61 | return 0, err 62 | } 63 | 64 | if premiumTier > premium.None { 65 | colourCode, ok, err := dbclient.Client.CustomColours.Get(ctx, guildId, colour.Int16()) 66 | if err != nil { 67 | return 0, err 68 | } else if !ok { 69 | return colour.Default(), nil 70 | } else { 71 | return colourCode, nil 72 | } 73 | } else { 74 | return colour.Default(), nil 75 | } 76 | } 77 | 78 | func EmbedFieldRaw(name, value string, inline bool) embed.EmbedField { 79 | return embed.EmbedField{ 80 | Name: name, 81 | Value: value, 82 | Inline: inline, 83 | } 84 | } 85 | 86 | func EmbedField(guildId uint64, name string, value i18n.MessageId, inline bool, format ...interface{}) embed.EmbedField { 87 | return embed.EmbedField{ 88 | Name: name, 89 | Value: i18n.GetMessageFromGuild(guildId, value, format...), 90 | Inline: inline, 91 | } 92 | } 93 | 94 | func BuildEmoji(emote string) *emoji.Emoji { 95 | return &emoji.Emoji{ 96 | Name: emote, 97 | } 98 | } 99 | 100 | func Embeds(embeds ...*embed.Embed) []*embed.Embed { 101 | return embeds 102 | } 103 | -------------------------------------------------------------------------------- /bot/utils/permissionutils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/permission" 6 | "github.com/TicketsBot/database" 7 | "github.com/TicketsBot/worker/bot/command/registry" 8 | "github.com/TicketsBot/worker/bot/dbclient" 9 | ) 10 | 11 | func CanClose(ctx context.Context, cmd registry.CommandContext, ticket database.Ticket) bool { 12 | // Make sure user can close; 13 | // Get user's permissions level 14 | permissionLevel, err := cmd.UserPermissionLevel(ctx) 15 | if err != nil { 16 | cmd.HandleError(err) 17 | return false 18 | } 19 | 20 | if permissionLevel == permission.Everyone { 21 | usersCanClose, err := dbclient.Client.UsersCanClose.Get(ctx, cmd.GuildId()) 22 | if err != nil { 23 | cmd.HandleError(err) 24 | } 25 | 26 | // If they are a normal user, don't let them close if users_can_close=false, or if they are not the opener 27 | if !usersCanClose || cmd.UserId() != ticket.UserId { 28 | return false 29 | } 30 | } 31 | 32 | return true 33 | } 34 | -------------------------------------------------------------------------------- /bot/utils/premium.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/TicketsBot/common/premium" 4 | 5 | var PremiumClient premium.IPremiumLookupClient 6 | -------------------------------------------------------------------------------- /bot/utils/proxyhook.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/TicketsBot/worker/config" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // Twilight's HTTP proxy doesn't support the typical HTTP proxy protocol - instead you send the request directly 10 | // to the proxy's host in the URL. This is not how Go's proxy function should be used, but it works :) 11 | func ProxyHook(token string, req *http.Request) { 12 | if !strings.HasPrefix(req.URL.Path, "/api/v9/applications/") { 13 | req.URL.Scheme = "http" 14 | req.URL.Host = config.Conf.Discord.ProxyUrl 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bot/utils/retriever.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/TicketsBot/common/permission" 7 | "github.com/TicketsBot/database" 8 | "github.com/TicketsBot/worker" 9 | "github.com/TicketsBot/worker/bot/dbclient" 10 | "github.com/TicketsBot/worker/bot/redis" 11 | "github.com/rxdn/gdl/cache" 12 | ) 13 | 14 | func ToRetriever(worker *worker.Context) permission.Retriever { 15 | return WorkerRetriever{ 16 | ctx: worker, 17 | } 18 | } 19 | 20 | type WorkerRetriever struct { 21 | ctx *worker.Context 22 | } 23 | 24 | func (wr WorkerRetriever) Db() *database.Database { 25 | return dbclient.Client 26 | } 27 | 28 | func (wr WorkerRetriever) Cache() permission.PermissionCache { 29 | return permission.NewRedisCache(redis.Client) 30 | } 31 | 32 | func (wr WorkerRetriever) IsBotAdmin(_ context.Context, userId uint64) bool { 33 | return IsBotAdmin(userId) 34 | } 35 | 36 | func (wr WorkerRetriever) GetGuildOwner(ctx context.Context, guildId uint64) (uint64, error) { 37 | cachedOwner, err := wr.ctx.Cache.GetGuildOwner(ctx, guildId) 38 | if err == nil { 39 | return cachedOwner, nil 40 | } else if !errors.Is(err, cache.ErrNotFound) { 41 | return 0, err 42 | } 43 | 44 | guild, err := wr.ctx.GetGuild(guildId) 45 | if err != nil { 46 | return 0, err 47 | } 48 | 49 | go wr.ctx.Cache.StoreGuild(ctx, guild) 50 | return guild.OwnerId, nil 51 | } 52 | -------------------------------------------------------------------------------- /bot/utils/sliceutils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func ToSlice[T any](slice ...T) []T { 4 | return slice 5 | } 6 | 7 | func Contains[T comparable](slice []T, target T) bool { 8 | for _, el := range slice { 9 | if el == target { 10 | return true 11 | } 12 | } 13 | 14 | return false 15 | } 16 | 17 | func ContainsFunc[T any](slice []T, f func(T) bool) bool { 18 | for _, el := range slice { 19 | if f(el) { 20 | return true 21 | } 22 | } 23 | 24 | return false 25 | } 26 | 27 | func HasIntersection[T comparable](slice []T, slice2 []T) bool { 28 | for _, el := range slice { 29 | for _, el2 := range slice2 { 30 | if el == el2 { 31 | return true 32 | } 33 | } 34 | } 35 | 36 | return false 37 | } 38 | 39 | func FindIntersection[T comparable](slice []T, slice2 []T) []T { 40 | var intersection []T 41 | for _, el := range slice { 42 | for _, el2 := range slice2 { 43 | if el == el2 { 44 | intersection = append(intersection, el) 45 | } 46 | } 47 | } 48 | 49 | return intersection 50 | } 51 | 52 | func Keys[T comparable, U any](m map[T]U) []T { 53 | keys := make([]T, 0, len(m)) 54 | for k := range m { 55 | keys = append(keys, k) 56 | } 57 | 58 | return keys 59 | } 60 | -------------------------------------------------------------------------------- /bot/utils/stringutils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "golang.org/x/exp/constraints" 5 | "math/rand" 6 | "strings" 7 | ) 8 | 9 | type Number interface { 10 | constraints.Integer | constraints.Float 11 | } 12 | 13 | func StringMax(str string, max int, suffix ...string) string { 14 | if len(str) > max { 15 | return str[:max] + strings.Join(suffix, "") 16 | } 17 | 18 | return str 19 | } 20 | 21 | func Max[T Number](a, b T) T { 22 | if a > b { 23 | return a 24 | } else { 25 | return b 26 | } 27 | } 28 | 29 | func Min[T Number](a, b T) T { 30 | if a < b { 31 | return a 32 | } else { 33 | return b 34 | } 35 | } 36 | 37 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 38 | 39 | func RandString(length int) string { 40 | b := make([]rune, length) 41 | for i := range b { 42 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 43 | } 44 | return string(b) 45 | } 46 | -------------------------------------------------------------------------------- /bot/utils/timeutils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func FormatTime(interval time.Duration) string { 9 | minutes := (interval.Milliseconds() / (1000 * 60)) % 60 10 | hours := (interval.Milliseconds() / (1000 * 60 * 60)) % 24 11 | 12 | return fmt.Sprintf("%dh %02dm", hours, minutes) 13 | } 14 | 15 | func FormatDateTime(time time.Time) string { 16 | zone, _ := time.Zone() 17 | return fmt.Sprintf("%d/%d/%d %d:%d (%s)", time.Day(), time.Month(), time.Year(), time.Hour(), time.Minute(), zone) 18 | } 19 | 20 | func FormatNullableTime(duration *time.Duration) string { 21 | if duration == nil { 22 | return "No data" 23 | } else { 24 | return FormatTime(*duration) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/exportmessages/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/TicketsBot/worker/bot/dbclient" 7 | "github.com/TicketsBot/worker/i18n" 8 | "io/ioutil" 9 | ) 10 | 11 | func main() { 12 | dbclient.Connect() 13 | translations, err := dbclient.Client.Translations.GetAll() 14 | must(err) 15 | 16 | for lang, msgs := range translations { 17 | newMsgs := make(map[i18n.MessageId]string) 18 | for i, msg := range msgs { 19 | msgId := i18n.Messages[i] 20 | newMsgs[msgId] = msg 21 | } 22 | 23 | encoded, err := json.MarshalIndent(newMsgs, "", " ") 24 | must(err) 25 | 26 | path := fmt.Sprintf("./locale/%s.json", lang) 27 | must(ioutil.WriteFile(path, encoded, 0)) 28 | } 29 | } 30 | 31 | func must(err error) { 32 | if err != nil { 33 | panic(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/registercommands/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "github.com/TicketsBot/worker/bot/command/manager" 9 | "github.com/TicketsBot/worker/i18n" 10 | "github.com/rxdn/gdl/objects/interaction" 11 | "github.com/rxdn/gdl/rest" 12 | ) 13 | 14 | var ( 15 | Token = flag.String("token", "", "Bot token to create commands for") 16 | ApplicationId = flag.Uint64("id", 508391840525975553, "Application ID") 17 | GuildId = flag.Uint64("guild", 0, "Guild to create the commands for") 18 | 19 | AdminCommandGuildId = flag.Uint64("admin-guild", 0, "Guild to create the admin commands in") 20 | MergeGuildCommands = flag.Bool("merge", true, "Don't overwrite existing commands") 21 | ) 22 | 23 | func main() { 24 | flag.Parse() 25 | if *Token == "" { 26 | panic("no token") 27 | } 28 | 29 | i18n.Init() 30 | 31 | commandManager := new(manager.CommandManager) 32 | commandManager.RegisterCommands() 33 | 34 | data, adminCommands := commandManager.BuildCreatePayload(false, AdminCommandGuildId) 35 | 36 | var err error 37 | if *GuildId == 0 { 38 | must(rest.ModifyGlobalCommands(context.Background(), *Token, nil, *ApplicationId, data)) 39 | } else { 40 | must(rest.ModifyGuildCommands(context.Background(), *Token, nil, *ApplicationId, *GuildId, data)) 41 | } 42 | 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | if AdminCommandGuildId != nil && *AdminCommandGuildId != 0 { 48 | if MergeGuildCommands != nil && *MergeGuildCommands { 49 | cmds := must(rest.GetGuildCommands(context.Background(), *Token, nil, *ApplicationId, *AdminCommandGuildId)) 50 | for _, cmd := range cmds { 51 | var found bool 52 | for _, newCmd := range adminCommands { 53 | if cmd.Name == newCmd.Name { 54 | found = true 55 | break 56 | } 57 | } 58 | 59 | if !found { 60 | adminCommands = append(adminCommands, rest.CreateCommandData{ 61 | Id: cmd.Id, 62 | Name: cmd.Name, 63 | Description: cmd.Description, 64 | Options: cmd.Options, 65 | Type: interaction.ApplicationCommandTypeChatInput, 66 | }) 67 | } 68 | } 69 | } 70 | 71 | must(rest.ModifyGuildCommands(context.Background(), *Token, nil, *ApplicationId, *AdminCommandGuildId, adminCommands)) 72 | } 73 | 74 | cmds := must(rest.GetGlobalCommands(context.Background(), *Token, nil, *ApplicationId)) 75 | marshalled := must(json.MarshalIndent(cmds, "", " ")) 76 | 77 | fmt.Println(string(marshalled)) 78 | } 79 | 80 | func must[T any](t T, err error) T { 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | return t 86 | } 87 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "github.com/rxdn/gdl/cache" 5 | "github.com/rxdn/gdl/objects/user" 6 | "github.com/rxdn/gdl/rest/ratelimit" 7 | ) 8 | 9 | type Context struct { 10 | Token string 11 | BotId uint64 12 | IsWhitelabel bool 13 | ShardId int 14 | Cache *cache.PgCache 15 | RateLimiter *ratelimit.Ratelimiter 16 | } 17 | 18 | func (ctx *Context) Self() (user.User, error) { 19 | return ctx.GetUser(ctx.BotId) 20 | } 21 | -------------------------------------------------------------------------------- /event/errorcontext.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "github.com/TicketsBot/worker/bot/utils" 5 | "github.com/rxdn/gdl/objects/interaction" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | type InteractionErrorContext struct { 11 | data map[string]string 12 | } 13 | 14 | func (ctx InteractionErrorContext) ToMap() map[string]string { 15 | return ctx.data 16 | } 17 | 18 | func NewApplicationCommandInteractionErrorContext(data interaction.ApplicationCommandInteraction) InteractionErrorContext { 19 | return InteractionErrorContext{ 20 | data: map[string]string{ 21 | "interaction_id": strconv.FormatUint(data.Id, 10), 22 | "interaction_timestamp": utils.SnowflakeToTime(data.Id).String(), 23 | "current_time": time.Now().String(), 24 | "command_name": data.Data.Name, 25 | }, 26 | } 27 | } 28 | 29 | func NewMessageComponentInteractionErrorContext(data interaction.InteractionMetadata) InteractionErrorContext { 30 | m := map[string]string{ 31 | "interaction_id": strconv.FormatUint(data.Id, 10), 32 | "interaction_timestamp": utils.SnowflakeToTime(data.Id).String(), 33 | "current_time": time.Now().String(), 34 | //"component_type": strconv.Itoa(int(data.Data.ComponentType)), 35 | } 36 | 37 | if !data.GuildId.IsNull { 38 | m["guild_id"] = strconv.FormatUint(data.GuildId.Value, 10) 39 | } 40 | 41 | if data.Member != nil { 42 | m["user_id"] = strconv.FormatUint(data.Member.User.Id, 10) 43 | } else if data.User != nil { 44 | m["user_id"] = strconv.FormatUint(data.User.Id, 10) 45 | } 46 | 47 | /* 48 | if data.Data.Type() == component.ComponentButton { 49 | m["custom_id"] = data.Data.AsButton().CustomId 50 | } else if data.Data.Type() == component.ComponentSelectMenu { 51 | m["custom_id"] = data.Data.AsSelectMenu().CustomId 52 | } 53 | */ 54 | 55 | return InteractionErrorContext{ 56 | data: m, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /event/eventexecutor.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/TicketsBot/worker" 8 | "github.com/TicketsBot/worker/bot/listeners" 9 | "github.com/TicketsBot/worker/bot/metrics/prometheus" 10 | "github.com/getsentry/sentry-go" 11 | "github.com/rxdn/gdl/gateway/payloads" 12 | ) 13 | 14 | func execute(c *worker.Context, event []byte) error { 15 | var payload payloads.Payload 16 | if err := json.Unmarshal(event, &payload); err != nil { 17 | return errors.New(fmt.Sprintf("error whilst decoding event data: %s (data: %s)", err.Error(), string(event))) 18 | } 19 | 20 | span := sentry.StartTransaction(context.Background(), "Handle Event") 21 | span.SetTag("event", payload.EventName) 22 | defer span.Finish() 23 | 24 | prometheus.Events.WithLabelValues(payload.EventName).Inc() 25 | 26 | if err := listeners.HandleEvent(c, span, payload); err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /event/json.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import jsoniter "github.com/json-iterator/go" 4 | 5 | var json = jsoniter.Config{ 6 | EscapeHTML: false, 7 | SortMapKeys: false, 8 | UseNumber: false, 9 | ObjectFieldMustBeSimpleString: false, 10 | }.Froze() 11 | -------------------------------------------------------------------------------- /event/kafkalisten.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "context" 5 | "github.com/TicketsBot/common/eventforwarding" 6 | "github.com/TicketsBot/common/rpc" 7 | "github.com/TicketsBot/worker" 8 | "github.com/rxdn/gdl/cache" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type KafkaConsumer struct { 13 | logger *zap.Logger 14 | cache *cache.PgCache 15 | } 16 | 17 | var _ rpc.Listener = (*KafkaConsumer)(nil) 18 | 19 | func NewKafkaListener(logger *zap.Logger, cache *cache.PgCache) *KafkaConsumer { 20 | return &KafkaConsumer{ 21 | logger: logger, 22 | cache: cache, 23 | } 24 | } 25 | 26 | func (k *KafkaConsumer) BuildContext() (context.Context, context.CancelFunc) { 27 | return context.WithCancel(context.Background()) 28 | } 29 | 30 | func (k *KafkaConsumer) HandleMessage(ctx context.Context, message []byte) { 31 | var event eventforwarding.Event 32 | if err := json.Unmarshal(message, &event); err != nil { 33 | k.logger.Error("Failed to unmarshal event", zap.Error(err)) 34 | return 35 | } 36 | 37 | workerCtx := &worker.Context{ 38 | Token: event.BotToken, 39 | BotId: event.BotId, 40 | IsWhitelabel: event.IsWhitelabel, 41 | ShardId: event.ShardId, 42 | Cache: k.cache, 43 | RateLimiter: nil, // Use http-proxy ratelimit functionality 44 | } 45 | 46 | if err := execute(workerCtx, event.Event); err != nil { 47 | k.logger.Error("Failed to handle event", zap.Error(err)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tools/cmd/generatecmdcaller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "github.com/TicketsBot/worker/bot/command" 7 | "github.com/TicketsBot/worker/bot/command/manager" 8 | "github.com/TicketsBot/worker/bot/command/registry" 9 | "github.com/TicketsBot/worker/bot/utils" 10 | "github.com/rxdn/gdl/objects/channel" 11 | "github.com/rxdn/gdl/objects/interaction" 12 | "os" 13 | "path/filepath" 14 | "reflect" 15 | "sort" 16 | "strings" 17 | "text/template" 18 | ) 19 | 20 | type executorData struct { 21 | ImportName string 22 | Arguments []command.Argument 23 | } 24 | 25 | //go:embed cmdcaller.tmpl 26 | var callerTemplate string 27 | 28 | var typeMap = map[interaction.ApplicationCommandOptionType]reflect.Type{ 29 | interaction.OptionTypeString: reflect.TypeOf(""), // string 30 | interaction.OptionTypeInteger: reflect.TypeOf(int(0)), // int 31 | interaction.OptionTypeBoolean: reflect.TypeOf(false), // bool 32 | interaction.OptionTypeUser: reflect.TypeOf(uint64(0)), // snowflake 33 | interaction.OptionTypeChannel: reflect.TypeOf(uint64(0)), // snowflake 34 | interaction.OptionTypeRole: reflect.TypeOf(uint64(0)), // snowflake 35 | interaction.OptionTypeMentionable: reflect.TypeOf(uint64(0)), // snowflake 36 | interaction.OptionTypeNumber: reflect.TypeOf(float64(0)), // float64 37 | interaction.OptionTypeAttachment: reflect.TypeOf(channel.Attachment{}), // attachment 38 | } 39 | 40 | func main() { 41 | cm := manager.CommandManager{} 42 | cm.RegisterCommands() 43 | 44 | allCmds := make([]registry.Command, 0, len(cm.GetCommands())) 45 | for _, cmd := range cm.GetCommands() { 46 | allCmds = append(allCmds, cmd) 47 | for _, sub := range cmd.Properties().Children { 48 | allCmds = append(allCmds, sub) 49 | } 50 | } 51 | 52 | var packagePaths []string 53 | var executors []executorData 54 | 55 | for _, cmd := range allCmds { 56 | t := reflect.TypeOf(cmd) 57 | pkg := t.PkgPath() 58 | if !utils.Contains(packagePaths, pkg) { 59 | packagePaths = append(packagePaths, pkg) 60 | } 61 | 62 | importName := pkg[strings.LastIndex(pkg, "/")+1:] + "." + t.Name() 63 | 64 | executors = append(executors, executorData{ 65 | ImportName: importName, 66 | Arguments: cmd.Properties().Arguments, 67 | }) 68 | } 69 | 70 | // Order executors by import name for reproducible builds 71 | sort.Slice(executors, func(i, j int) bool { 72 | return executors[i].ImportName < executors[j].ImportName 73 | }) 74 | 75 | tmpl, err := template.New("caller"). 76 | Funcs(template.FuncMap{ 77 | "panic": func(msg string) string { 78 | panic(msg) 79 | }, 80 | }). 81 | Parse(callerTemplate) 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | var buf bytes.Buffer 87 | if err := tmpl.Execute(&buf, map[string]any{ 88 | "imports": packagePaths, 89 | "executors": executors, 90 | "typeMap": typeMap, 91 | }); err != nil { 92 | panic(err) 93 | } 94 | 95 | path := filepath.Join(filepath.Dir("."), "caller.go") 96 | if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil { 97 | panic(err) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tools/cmd/generatelisteners.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | _ "github.com/rxdn/gdl/gateway/payloads/events" 7 | "go/types" 8 | "golang.org/x/tools/go/packages" 9 | "html/template" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "unicode" 14 | ) 15 | 16 | const ( 17 | PackageName = "github.com/rxdn/gdl/gateway/payloads/events" 18 | ) 19 | 20 | //go:embed listeners.tmpl 21 | var listenersTemplate string 22 | 23 | func main() { 24 | cfg := &packages.Config{ 25 | Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesInfo, 26 | } 27 | 28 | pkgs, err := packages.Load(cfg, PackageName) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | if len(pkgs) != 1 { 34 | panic("expected 1 package") 35 | } 36 | 37 | pkg := pkgs[0] 38 | scope := pkg.Types.Scope() 39 | 40 | events := make([]string, 0) 41 | 42 | for _, name := range scope.Names() { 43 | object := scope.Lookup(name) 44 | typeName := strings.TrimPrefix(object.Type().String(), PackageName+".") 45 | 46 | if typeName == object.Name() { 47 | if typeName == "EventBus" { 48 | continue 49 | } 50 | 51 | // Check if underlying type is a struct 52 | underlying := object.Type().Underlying() 53 | if _, ok := underlying.(*types.Struct); ok { 54 | events = append(events, object.Name()) 55 | } 56 | } 57 | } 58 | 59 | tmpl, err := template. 60 | New("listeners"). 61 | Funcs(template.FuncMap{ 62 | "toScreamingSnakeCase": func(orig string) string { 63 | buf := strings.Builder{} 64 | for i, r := range orig { 65 | if i > 0 && 'A' <= r && r <= 'Z' { 66 | buf.WriteRune('_') 67 | } 68 | 69 | buf.WriteRune(unicode.ToUpper(r)) 70 | } 71 | 72 | return buf.String() 73 | }, 74 | }). 75 | Parse(listenersTemplate) 76 | if err != nil { 77 | panic(err) 78 | } 79 | 80 | var buf bytes.Buffer 81 | if err := tmpl.Execute(&buf, map[string]any{ 82 | "events": events, 83 | }); err != nil { 84 | panic(err) 85 | } 86 | 87 | path := filepath.Join(filepath.Dir("."), "listeners.go") 88 | if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil { 89 | panic(err) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tools/cmd/listeners.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by /tools/cmd/generatelisteners.go; DO NOT EDIT. 2 | //go:generate go run ../../tools/cmd/generatelisteners.go 3 | 4 | package listeners 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "github.com/TicketsBot/worker" 10 | "github.com/getsentry/sentry-go" 11 | "github.com/rxdn/gdl/gateway/payloads" 12 | "github.com/rxdn/gdl/gateway/payloads/events" 13 | ) 14 | 15 | var ( 16 | {{range .events}} 17 | {{.}}Listeners = []func(*worker.Context, events.{{.}}){}{{end}} 18 | ) 19 | 20 | func HandleEvent(c *worker.Context, span *sentry.Span, payload payloads.Payload) error { 21 | if payload.Opcode != 0 { // Dispatch 22 | return fmt.Errorf("HandleEvent called with non-dispatch op-code: %d", payload.Opcode) 23 | } 24 | 25 | switch events.EventType(payload.EventName) { 26 | {{range .events}} 27 | case events.{{toScreamingSnakeCase .}}: 28 | var event events.{{.}} 29 | if err := json.Unmarshal(payload.Data, &event); err != nil { 30 | return err 31 | } 32 | 33 | for _, listener := range {{.}}Listeners { 34 | listener(c, event) 35 | } 36 | {{end}} 37 | default: 38 | return fmt.Errorf("Unknown event type: %s", payload.EventName) 39 | } 40 | 41 | return nil 42 | } 43 | --------------------------------------------------------------------------------