Skip to content

Commit

Permalink
Make tier IDs configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
rxdn committed Jul 21, 2024
1 parent 713ef58 commit 88e392e
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 58 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ PATREON_CLIENT_ID=your_client_id
PATREON_CLIENT_SECRET=your_client_secret
PATREON_CAMPAIGN_ID=your_campaign_id
PATREON_TOKENS_FILE_PATH=/data/tokens.json
TIERS=12345:Super,67890:Ultra
SERVER_ADDR=:8080
PRODUCTION_MODE=true
SENTRY_DSN=your_sentry_dsn
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea/
*.iml
.env
tokens.json
tokens.json
config.json
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ Some experience with Discord app development is assumed.
refresh token in a `tokens.json` file. An example is provided in [`tokens.json.example`](/tokens.json.example).
4. Run the main binary: there are 2 ways of doing this - either by building and running the main binary directly
(`go build cmd/app/main.go`), or via Docker (recommended). If running the binary directly, see the
[envvars.md](/envvars.md) file for a list of environment variables that need to be set.
[envvars.md](/envvars.md) file for a list of environment variables that need to be set.


Alternatively, a `config.json` file can be used to configure the application. See the
[config.json.example](/config.json.example) file for an example.

Note, anyone is able to use the command, as long as the command is run in a guild listed in the `DISCORD_ALLOWED_GUILDS`
environment variable. You should use Discord's built-in application command permission system to restrict usage to
Expand Down
60 changes: 32 additions & 28 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,39 @@ func main() {

var logger *zap.Logger
if conf.ProductionMode {
if err := sentry.Init(sentry.ClientOptions{
Dsn: conf.SentryDsn,
}); err != nil {
panic(err)
}
if conf.SentryDsn != nil {
if err := sentry.Init(sentry.ClientOptions{
Dsn: *conf.SentryDsn,
}); err != nil {
panic(err)
}

defer sentry.Flush(time.Second * 2)

logger, err = zap.NewProduction(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
return zapcore.RegisterHooks(core, func(entry zapcore.Entry) error {
if entry.Level == zapcore.ErrorLevel {
hostname, _ := os.Hostname()

sentry.CaptureEvent(&sentry.Event{
Extra: map[string]any{
"caller": entry.Caller.String(),
"stack": entry.Stack,
},
Level: sentry.LevelError,
Message: entry.Message,
ServerName: hostname,
Timestamp: entry.Time,
Logger: entry.LoggerName,
})
}

return nil
})
}))
defer sentry.Flush(time.Second * 2)

logger, err = zap.NewProduction(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
return zapcore.RegisterHooks(core, func(entry zapcore.Entry) error {
if entry.Level == zapcore.ErrorLevel {
hostname, _ := os.Hostname()

sentry.CaptureEvent(&sentry.Event{
Extra: map[string]any{
"caller": entry.Caller.String(),
"stack": entry.Stack,
},
Level: sentry.LevelError,
Message: entry.Message,
ServerName: hostname,
Timestamp: entry.Time,
Logger: entry.LoggerName,
})
}

return nil
})
}))
} else {
logger, err = zap.NewProduction()
}
} else {
logger, err = zap.NewDevelopment()
}
Expand Down
19 changes: 19 additions & 0 deletions config.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"server_address": "0.0.0.0:8080",
"production_mode": true,
"sentry_dsn": null,
"discord": {
"public_key": "",
"allowed_guilds": [12345678901234567]
},
"patreon": {
"client_id": "",
"client_secret": "",
"campaign_id": 1111111,
"tokens_file_path": "tokens.json"
},
"tiers": {
"1234": "Super",
"5678": "Ultra"
}
}
17 changes: 9 additions & 8 deletions envvars.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
- DISCORD_PUBLIC_KEY
- DISCORD_ALLOWED_GUILDS
- PATREON_CLIENT_ID
- PATREON_CLIENT_SECRET
- PATREON_CAMPAIN_ID
- SERVER_ADDR
- SENTRY_DSN
- PRODUCTION_MODE
- **DISCORD_PUBLIC_KEY**: The public key for your Discord application to verify interactions.
- **DISCORD_ALLOWED_GUILDS**: A comma-separated list of Discord guild IDs that commands will be accepted in.
- **PATREON_CLIENT_ID**: The client ID string for your Patreon app.
- **PATREON_CLIENT_SECRET**: The client secret string for your Patreon app.
- **PATREON_CAMPAIGN_ID**: The ID of the Patreon campaign to use for fetching pledges.
- **SERVER_ADDR**: The address to bind the web server for HTTP interactions to (e.g. `:8080).
- **SENTRY_DSN**: Optional, used for error reporting.
- **PRODUCTION_MODE**: Currently only used to determine the log format.
- **TIERS**: A comma-separated list of Patreon tier IDs and names, in the format `1234:Name,5678:Name`, and so on.
54 changes: 40 additions & 14 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,54 @@
package config

import (
"encoding/json"
"github.com/caarlos0/env/v9"
"github.com/pkg/errors"
"os"
)

type Config struct {
ServerAddr string `env:"SERVER_ADDR,required"`
ProductionMode bool `env:"PRODUCTION_MODE" envDefault:"false"`
SentryDsn string `env:"SENTRY_DSN"`
ServerAddr string `env:"SERVER_ADDR,required" json:"server_address"`
ProductionMode bool `env:"PRODUCTION_MODE" envDefault:"false" json:"production_mode"`
SentryDsn *string `env:"SENTRY_DSN" json:"sentry_dsn"`

Discord struct {
PublicKey string `env:"PUBLIC_KEY,required"`
AllowedGuilds []uint64 `env:"ALLOWED_GUILDS,required"`
} `envPrefix:"DISCORD_"`
PublicKey string `env:"PUBLIC_KEY,required" json:"public_key"`
AllowedGuilds []uint64 `env:"ALLOWED_GUILDS,required" json:"allowed_guilds"`
} `envPrefix:"DISCORD_" json:"discord"`

Patreon struct {
ClientId string `env:"CLIENT_ID,required"`
ClientSecret string `env:"CLIENT_SECRET,required"`
CampaignId int `env:"CAMPAIGN_ID,required"`
TokensFilePath string `env:"TOKENS_FILE_PATH" envDefault:"tokens.json"`
} `envPrefix:"PATREON_"`
ClientId string `env:"CLIENT_ID,required" json:"client_id"`
ClientSecret string `env:"CLIENT_SECRET,required" json:"client_secret"`
CampaignId int `env:"CAMPAIGN_ID,required" json:"campaign_id"`
TokensFilePath string `env:"TOKENS_FILE_PATH" envDefault:"tokens.json" json:"tokens_file_path"`
} `envPrefix:"PATREON_" json:"patreon"`

Tiers map[uint64]string `env:"TIERS" json:"tiers"`
}

func LoadConfig() (conf Config, err error) {
err = env.Parse(&conf)
return
func LoadConfig() (Config, error) {
var conf Config
if _, err := os.Stat("config.json"); err == nil {
f, err := os.Open("config.json")
if err != nil {
return Config{}, errors.Wrap(err, "failed to open config.json")
}

if err := json.NewDecoder(f).Decode(&conf); err != nil {
return Config{}, errors.Wrap(err, "failed to decode config.json")
}

if conf.Patreon.TokensFilePath == "" {
conf.Patreon.TokensFilePath = "tokens.json"
}
} else if errors.Is(err, os.ErrNotExist) { // If config.json does not exist, load from envvars
if err := env.Parse(&conf); err != nil {
return Config{}, errors.Wrap(err, "failed to parse env vars")
}
} else {
return conf, errors.Wrap(err, "failed to check if config.json exists")
}

return conf, nil
}
7 changes: 6 additions & 1 deletion internal/server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,12 @@ func handleCommand(s *Server, data interaction.ApplicationCommandInteraction) in
if ok {
tiers := make([]string, len(patron.Tiers))
for i, tier := range patron.Tiers {
tiers[i] = tier.String()
tierName, ok := s.config.Tiers[tier]
if !ok {
tierName = fmt.Sprintf("Unknown (ID: %d)", tier)
}

tiers[i] = tierName
}

discord := "Not linked"
Expand Down
8 changes: 4 additions & 4 deletions pkg/patreon/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ func (c *Client) FetchPledges(ctx context.Context) (map[string]Patron, error) {
}

// Parse tiers
var tiers []Tier
var tiers []uint64
for _, tier := range member.Relationships.CurrentlyEntitledTiers.Data {
internalTier, ok := GetTierFromId(tier.TierId)
if !ok {
// Check if tier is known
if _, ok := c.config.Tiers[tier.TierId]; !ok {
c.logger.Warn("unknown tier", zap.Uint64("tier_id", tier.TierId))
continue
}

tiers = append(tiers, internalTier)
tiers = append(tiers, tier.TierId)
}

// Parse "included" metadata
Expand Down
2 changes: 1 addition & 1 deletion pkg/patreon/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type (
Patron struct {
Attributes
Id uint64
Tiers []Tier
Tiers []uint64
DiscordId *uint64
}

Expand Down

0 comments on commit 88e392e

Please sign in to comment.