feat: notifications - i18n (#112)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details

Reviewed-on: #112
This commit is contained in:
Dawid Wysokiński 2023-06-30 05:09:52 +00:00
parent 6415b162de
commit 4012bd0a2f
18 changed files with 510 additions and 131 deletions

View File

@ -31,6 +31,7 @@ func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (do
ServerKey: params.ServerKey(),
Barbarians: params.Barbarians(),
Internals: params.Internals(),
LanguageTag: params.LanguageTag(),
}
if _, err := g.db.NewInsert().
@ -232,6 +233,14 @@ func (u updateGroupsParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery {
}
}
if u.params.LanguageTag.Valid {
if u.params.LanguageTag.String != "" {
q = q.Set("language_tag = ?", u.params.LanguageTag.String)
} else {
q = q.Set("language_tag = NULL")
}
}
if u.params.Barbarians.Valid {
q = q.Set("barbarians = ?", u.params.Barbarians.Bool)
}

View File

@ -10,6 +10,7 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
)
func TestGroup_Create(t *testing.T) {
@ -28,6 +29,7 @@ func TestGroup_Create(t *testing.T) {
"592292203234328587",
"en",
"en113",
language.Polish.String(),
"1234",
"1235",
true,
@ -45,6 +47,7 @@ func TestGroup_Create(t *testing.T) {
assert.Equal(t, params.ChannelLosses(), group.ChannelLosses)
assert.Equal(t, params.Barbarians(), group.Barbarians)
assert.Equal(t, params.Internals(), group.Internals)
assert.Equal(t, params.LanguageTag(), group.LanguageTag)
assert.WithinDuration(t, time.Now(), group.CreatedAt, 1*time.Second)
})
}
@ -73,6 +76,10 @@ func TestGroup_Update(t *testing.T) {
String: group.ChannelLosses + "update",
Valid: true,
},
LanguageTag: domain.NullString{
String: language.AmericanEnglish.String(),
Valid: true,
},
Barbarians: domain.NullBool{
Bool: !group.Barbarians,
Valid: true,
@ -87,6 +94,7 @@ func TestGroup_Update(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, params.ChannelGains.String, updatedGroup.ChannelGains)
assert.Equal(t, params.ChannelLosses.String, updatedGroup.ChannelLosses)
assert.Equal(t, params.LanguageTag.String, updatedGroup.LanguageTag)
assert.Equal(t, params.Barbarians.Bool, updatedGroup.Barbarians)
assert.Equal(t, params.Internals.Bool, updatedGroup.Internals)
})

View File

@ -19,6 +19,7 @@ type Group struct {
Barbarians bool `bun:"barbarians"`
ServerKey string `bun:"server_key,nullzero"`
VersionCode string `bun:"version_code,nullzero"`
LanguageTag string `bun:"language_tag,nullzero"`
CreatedAt time.Time `bun:"created_at,nullzero"`
Monitors []Monitor `bun:"monitors,rel:has-many,join:id=group_id"`
}
@ -38,6 +39,7 @@ func (g Group) ToDomain() domain.GroupWithMonitors {
Barbarians: g.Barbarians,
ServerKey: g.ServerKey,
VersionCode: g.VersionCode,
LanguageTag: g.LanguageTag,
CreatedAt: g.CreatedAt,
},
Monitors: monitors,

View File

@ -6,6 +6,7 @@
server_key: pl181
version_code: pl
channel_losses: 125
language_tag: pl
created_at: 2022-03-15T15:00:10.000Z
- _id: group-2-server-1
id: 429b790e-7186-4106-b531-4cc4931ce2ba
@ -13,12 +14,14 @@
server_key: pl181
version_code: pl
channel_gains: 125
language_tag: pl
created_at: 2022-03-15T15:03:10.000Z
- _id: group-1-server-2
id: abeb6c8e-70b6-445c-989f-890cd2a1f87a
server_id: server-2
server_key: pl181
version_code: pl
language_tag: pl
created_at: 2022-03-18T15:03:10.000Z
- _id: group-2-server-2
id: 0be82203-4ca3-4b4c-a0c8-3a70099d88f7
@ -27,6 +30,7 @@
version_code: pl
channel_gains: 1555
channel_losses: 1235
language_tag: pl
created_at: 2022-03-19T15:03:10.000Z
- _id: group-3-server-1
id: 982a9765-471c-43e8-9abb-1e4ee4f03738
@ -35,6 +39,7 @@
version_code: pl
channel_gains: 1555
channel_losses: 1235
language_tag: pl
created_at: 2022-03-23T18:03:10.000Z
- _id: group-3-server-2
id: f3a0a5d9-07fe-4770-8b43-79cbe10bb310
@ -43,6 +48,7 @@
version_code: en
channel_gains: 1555
channel_losses: 1235
language_tag: en
created_at: 2022-04-23T18:03:10.000Z
- model: Monitor
rows:
@ -60,4 +66,4 @@
id: c7d63c3d-55f6-432e-b9d8-006f8c5ab407
group_id: 0be82203-4ca3-4b4c-a0c8-3a70099d88f7
tribe_id: 121
created_at: 2022-03-19T15:10:10.000Z
created_at: 2022-03-19T15:10:10.000Z

View File

@ -17,6 +17,7 @@ type GroupService interface {
Create(ctx context.Context, params domain.CreateGroupParams) (domain.GroupWithMonitors, error)
AddTribe(ctx context.Context, id, serverID, tribeTag string) (domain.GroupWithMonitors, error)
RemoveTribe(ctx context.Context, id, serverID, tribeTag string) (domain.GroupWithMonitors, error)
SetLanguageTag(ctx context.Context, id, serverID, languageTag string) (domain.GroupWithMonitors, error)
SetChannelGains(ctx context.Context, id, serverID, channel string) (domain.GroupWithMonitors, error)
SetChannelLosses(ctx context.Context, id, serverID, channel string) (domain.GroupWithMonitors, error)
SetInternals(ctx context.Context, id, serverID string, internals bool) (domain.GroupWithMonitors, error)
@ -54,7 +55,7 @@ func NewBot(
s, err := discordgo.New("Bot " + token)
if err != nil {
return nil, fmt.Errorf("discordgo.New: %w", err)
return nil, err
}
s.Identify.Intents = discordgo.IntentsNone
@ -80,16 +81,16 @@ func NewBot(
func (b *Bot) Run() error {
if err := b.registerCronJobs(); err != nil {
return fmt.Errorf("initCron: %w", err)
return err
}
if err := b.session.Open(); err != nil {
return fmt.Errorf("s.Open: %w", err)
return err
}
if err := b.registerCommands(); err != nil {
_ = b.session.Close()
return fmt.Errorf("couldn't register commands: %w", err)
return err
}
b.cron.Run()
@ -123,7 +124,7 @@ func (b *Bot) registerCronJobs() error {
}{
{
spec: "@every 1m",
job: &executeMonitorsJob{svc: b.groupSvc, s: b.session, logger: b.logger},
job: &executeMonitorsJob{svc: b.groupSvc, s: b.session, localizer: b.localizer, logger: b.logger},
},
{
spec: "0 */8 * * *",

View File

@ -80,7 +80,7 @@ func (c *groupCommand) create(s *discordgo.Session) error {
}
func (c *groupCommand) buildSubcommandCreate() (*discordgo.ApplicationCommandOption, error) {
versionChoices, err := c.getVersionChoices()
options, err := c.buildSubcommandCreateOptions()
if err != nil {
return nil, err
}
@ -90,6 +90,26 @@ func (c *groupCommand) buildSubcommandCreate() (*discordgo.ApplicationCommandOpt
return nil, err
}
return &discordgo.ApplicationCommandOption{
Name: "create",
Description: cmdDescriptionDefault,
DescriptionLocalizations: cmdDescriptionLocalizations,
Type: discordgo.ApplicationCommandOptionSubCommand,
Options: options,
}, nil
}
func (c *groupCommand) buildSubcommandCreateOptions() ([]*discordgo.ApplicationCommandOption, error) {
versionChoices, err := c.getVersionChoices()
if err != nil {
return nil, err
}
languageChoices, err := c.getLanguageChoices()
if err != nil {
return nil, err
}
optionVersionDefault, optionVersionLocalizations, err := c.localizer.LocalizeDiscord("cmd.group.create.option.version.description")
if err != nil {
return nil, err
@ -115,6 +135,11 @@ func (c *groupCommand) buildSubcommandCreate() (*discordgo.ApplicationCommandOpt
return nil, err
}
optionLanguageDefault, optionLanguageLocalizations, err := c.localizer.LocalizeDiscord("cmd.group.create.option.language.description")
if err != nil {
return nil, err
}
optionChannelLossesDefault, optionChannelLossesLocalizations, err := c.localizer.LocalizeDiscord(
"cmd.group.create.option.channel-losses.description",
)
@ -122,65 +147,101 @@ func (c *groupCommand) buildSubcommandCreate() (*discordgo.ApplicationCommandOpt
return nil, err
}
return &discordgo.ApplicationCommandOption{
Name: "create",
Description: cmdDescriptionDefault,
DescriptionLocalizations: cmdDescriptionLocalizations,
Type: discordgo.ApplicationCommandOptionSubCommand,
Options: []*discordgo.ApplicationCommandOption{
{
Name: "version",
Description: optionVersionDefault,
DescriptionLocalizations: optionVersionLocalizations,
Type: discordgo.ApplicationCommandOptionString,
Choices: versionChoices,
Required: true,
return []*discordgo.ApplicationCommandOption{
{
Name: "version",
Description: optionVersionDefault,
DescriptionLocalizations: optionVersionLocalizations,
Type: discordgo.ApplicationCommandOptionString,
Choices: versionChoices,
Required: true,
},
{
Name: "server",
Description: optionServerDefault,
DescriptionLocalizations: optionServerLocalizations,
Type: discordgo.ApplicationCommandOptionString,
Required: true,
},
{
Name: "language",
Description: optionLanguageDefault,
DescriptionLocalizations: optionLanguageLocalizations,
Type: discordgo.ApplicationCommandOptionString,
Choices: languageChoices,
Required: true,
},
{
Name: "internals",
Description: optionInternalsDefault,
DescriptionLocalizations: optionInternalsLocalizations,
Type: discordgo.ApplicationCommandOptionBoolean,
Required: true,
},
{
Name: "barbarians",
Description: optionBarbariansDefault,
DescriptionLocalizations: optionBarbariansLocalizations,
Type: discordgo.ApplicationCommandOptionBoolean,
Required: true,
},
{
Name: "channel-gains",
Description: optionChannelGainsDefault,
DescriptionLocalizations: optionChannelGainsLocalizations,
Type: discordgo.ApplicationCommandOptionChannel,
ChannelTypes: []discordgo.ChannelType{
discordgo.ChannelTypeGuildText,
},
{
Name: "server",
Description: optionServerDefault,
DescriptionLocalizations: optionServerLocalizations,
Type: discordgo.ApplicationCommandOptionString,
Required: true,
},
{
Name: "internals",
Description: optionInternalsDefault,
DescriptionLocalizations: optionInternalsLocalizations,
Type: discordgo.ApplicationCommandOptionBoolean,
Required: true,
},
{
Name: "barbarians",
Description: optionBarbariansDefault,
DescriptionLocalizations: optionBarbariansLocalizations,
Type: discordgo.ApplicationCommandOptionBoolean,
Required: true,
},
{
Name: "channel-gains",
Description: optionChannelGainsDefault,
DescriptionLocalizations: optionChannelGainsLocalizations,
Type: discordgo.ApplicationCommandOptionChannel,
ChannelTypes: []discordgo.ChannelType{
discordgo.ChannelTypeGuildText,
},
Required: false,
},
{
Name: "channel-losses",
Description: optionChannelLossesDefault,
DescriptionLocalizations: optionChannelLossesLocalizations,
Type: discordgo.ApplicationCommandOptionChannel,
ChannelTypes: []discordgo.ChannelType{
discordgo.ChannelTypeGuildText,
},
Required: false,
Required: false,
},
{
Name: "channel-losses",
Description: optionChannelLossesDefault,
DescriptionLocalizations: optionChannelLossesLocalizations,
Type: discordgo.ApplicationCommandOptionChannel,
ChannelTypes: []discordgo.ChannelType{
discordgo.ChannelTypeGuildText,
},
Required: false,
},
}, nil
}
func (c *groupCommand) getVersionChoices() ([]*discordgo.ApplicationCommandOptionChoice, error) {
choices, err := c.choiceSvc.Versions(context.Background())
if err != nil {
return nil, fmt.Errorf("ChoiceService.Versions: %w", err)
}
dcChoices := make([]*discordgo.ApplicationCommandOptionChoice, 0, len(choices))
for _, v := range choices {
dcChoices = append(dcChoices, &discordgo.ApplicationCommandOptionChoice{
Name: v.Name,
Value: v.Value,
})
}
return dcChoices, nil
}
func (c *groupCommand) getLanguageChoices() ([]*discordgo.ApplicationCommandOptionChoice, error) {
tags := c.localizer.LanguageTags()
choices := make([]*discordgo.ApplicationCommandOptionChoice, 0, len(tags))
for _, tag := range tags {
lang, langLocalizations, err := c.localizer.LocalizeDiscord(buildLangMessageID(tag.String()))
if err != nil {
return nil, err
}
choices = append(choices, &discordgo.ApplicationCommandOptionChoice{
Name: lang,
NameLocalizations: langLocalizations,
Value: tag.String(),
})
}
return choices, nil
}
func (c *groupCommand) buildSubcommandList() (*discordgo.ApplicationCommandOption, error) {
cmdDescriptionDefault, cmdDescriptionLocalizations, err := c.localizer.LocalizeDiscord("cmd.group.list.description")
if err != nil {
@ -329,6 +390,7 @@ func (c *groupCommand) buildSubcommandMonitorRemove() (*discordgo.ApplicationCom
func (c *groupCommand) buildSubcommandSet() (*discordgo.ApplicationCommandOption, error) {
subcommandBuilders := []func() (*discordgo.ApplicationCommandOption, error){
c.buildSubcommandSetLanguage,
c.buildSubcommandSetChannelGains,
c.buildSubcommandSetChannelLosses,
c.buildSubcommandSetInternals,
@ -357,6 +419,52 @@ func (c *groupCommand) buildSubcommandSet() (*discordgo.ApplicationCommandOption
}, nil
}
func (c *groupCommand) buildSubcommandSetLanguage() (*discordgo.ApplicationCommandOption, error) {
languageChoices, err := c.getLanguageChoices()
if err != nil {
return nil, err
}
cmdDescriptionDefault, cmdDescriptionLocalizations, err := c.localizer.LocalizeDiscord("cmd.group.set.language.description")
if err != nil {
return nil, err
}
optionGroupDefault, optionGroupLocalizations, err := c.localizer.LocalizeDiscord("cmd.group.set.language.option.group.description")
if err != nil {
return nil, err
}
optionLanguageDefault, optionLanguageLocalizations, err := c.localizer.LocalizeDiscord("cmd.group.set.language.option.language.description")
if err != nil {
return nil, err
}
return &discordgo.ApplicationCommandOption{
Name: "language",
Description: cmdDescriptionDefault,
DescriptionLocalizations: cmdDescriptionLocalizations,
Type: discordgo.ApplicationCommandOptionSubCommand,
Options: []*discordgo.ApplicationCommandOption{
{
Name: "group",
Description: optionGroupDefault,
DescriptionLocalizations: optionGroupLocalizations,
Type: discordgo.ApplicationCommandOptionString,
Required: true,
},
{
Name: "language",
Description: optionLanguageDefault,
DescriptionLocalizations: optionLanguageLocalizations,
Type: discordgo.ApplicationCommandOptionString,
Choices: languageChoices,
Required: true,
},
},
}, nil
}
func (c *groupCommand) buildSubcommandSetChannelGains() (*discordgo.ApplicationCommandOption, error) {
cmdDescriptionDefault, cmdDescriptionLocalizations, err := c.localizer.LocalizeDiscord("cmd.group.set.channel-gains.description")
if err != nil {
@ -549,23 +657,6 @@ func (c *groupCommand) buildSubcommandDelete() (*discordgo.ApplicationCommandOpt
}, nil
}
func (c *groupCommand) getVersionChoices() ([]*discordgo.ApplicationCommandOptionChoice, error) {
choices, err := c.choiceSvc.Versions(context.Background())
if err != nil {
return nil, fmt.Errorf("ChoiceService.Versions: %w", err)
}
dcChoices := make([]*discordgo.ApplicationCommandOptionChoice, 0, len(choices))
for _, v := range choices {
dcChoices = append(dcChoices, &discordgo.ApplicationCommandOptionChoice{
Name: v.Name,
Value: v.Value,
})
}
return dcChoices, nil
}
func (c *groupCommand) handle(s *discordgo.Session, i *discordgo.InteractionCreate) {
cmdData := i.ApplicationCommandData()
@ -575,7 +666,8 @@ func (c *groupCommand) handle(s *discordgo.Session, i *discordgo.InteractionCrea
ctx := context.Background()
switch cmdData.Options[0].Name {
name := cmdData.Options[0].Name
switch name {
case "create":
c.handleCreate(ctx, s, i)
case "list":
@ -588,6 +680,8 @@ func (c *groupCommand) handle(s *discordgo.Session, i *discordgo.InteractionCrea
c.handleTribe(ctx, s, i)
case "delete":
c.handleDelete(ctx, s, i)
default:
c.logger.Error("unknown subcommand", zap.String("command", "group"), zap.String("subcommand", name))
}
}
@ -647,12 +741,15 @@ func (c *groupCommand) optionsToCreateGroupParams(
channelLosses := ""
internals := false
barbarians := false
languageTag := ""
for _, opt := range options {
switch opt.Name {
case "version":
version = opt.StringValue()
case "server":
server = opt.StringValue()
case "language":
languageTag = opt.StringValue()
case "channel-gains":
channelGains = opt.ChannelValue(s).ID
case "channel-losses":
@ -663,8 +760,16 @@ func (c *groupCommand) optionsToCreateGroupParams(
barbarians = opt.BoolValue()
}
}
return domain.NewCreateGroupParams(guildID, version, server, channelGains, channelLosses, barbarians, internals)
return domain.NewCreateGroupParams(
guildID,
version,
server,
languageTag,
channelGains,
channelLosses,
barbarians,
internals,
)
}
func (c *groupCommand) handleList(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
@ -688,10 +793,21 @@ func (c *groupCommand) handleList(ctx context.Context, s *discordgo.Session, i *
if g.ChannelGains != "" {
channelGains = buildChannelMention(g.ChannelGains)
}
channelLosses := "-"
if g.ChannelLosses != "" {
channelLosses = buildChannelMention(g.ChannelLosses)
}
langMessageID := buildLangMessageID(g.LanguageTag)
lang, localizeErr := c.localizer.Localize(locale, &i18n.LocalizeConfig{
MessageID: langMessageID,
})
if localizeErr != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", langMessageID), zap.Error(localizeErr))
return
}
fieldValue, localizeErr := c.localizer.Localize(locale, &i18n.LocalizeConfig{
MessageID: fieldValueMessageID,
TemplateData: map[string]any{
@ -701,12 +817,14 @@ func (c *groupCommand) handleList(ctx context.Context, s *discordgo.Session, i *
"Internals": boolToEmoji(g.Internals),
"Barbarians": boolToEmoji(g.Barbarians),
"NumTribes": len(g.Monitors),
"Language": lang,
},
})
if localizeErr != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", fieldValueMessageID), zap.Error(localizeErr))
return
}
fields = append(fields, &discordgo.MessageEmbedField{
Name: g.ID,
Value: fieldValue,
@ -770,6 +888,15 @@ func (c *groupCommand) handleDetails(ctx context.Context, s *discordgo.Session,
channelLosses = buildChannelMention(g.ChannelLosses)
}
langMessageID := buildLangMessageID(g.LanguageTag)
lang, localizeErr := c.localizer.Localize(locale, &i18n.LocalizeConfig{
MessageID: langMessageID,
})
if localizeErr != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", langMessageID), zap.Error(localizeErr))
return
}
descriptionMessageID := "cmd.group.details.embed.description"
description, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{
MessageID: descriptionMessageID,
@ -780,6 +907,7 @@ func (c *groupCommand) handleDetails(ctx context.Context, s *discordgo.Session,
"Internals": boolToEmoji(g.Internals),
"Barbarians": boolToEmoji(g.Barbarians),
"Tribes": builderTribes.String(),
"Language": lang,
},
})
if err != nil {
@ -802,7 +930,10 @@ func (c *groupCommand) handleDetails(ctx context.Context, s *discordgo.Session,
}
func (c *groupCommand) handleSet(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
switch i.ApplicationCommandData().Options[0].Options[0].Name {
name := i.ApplicationCommandData().Options[0].Options[0].Name
switch name {
case "language":
c.handleSetLanguage(ctx, s, i)
case "channel-gains":
c.handleSetChannelGains(ctx, s, i)
case "channel-losses":
@ -811,9 +942,54 @@ func (c *groupCommand) handleSet(ctx context.Context, s *discordgo.Session, i *d
c.handleSetInternals(ctx, s, i)
case "barbarians":
c.handleSetBarbarians(ctx, s, i)
default:
c.logger.Error("unknown subcommand", zap.String("command", "group set"), zap.String("subcommand", name))
}
}
func (c *groupCommand) handleSetLanguage(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
locale := string(i.Locale)
group := ""
languageTag := ""
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
if opt == nil {
continue
}
switch opt.Name {
case "group":
group = opt.StringValue()
case "language":
languageTag = opt.StringValue()
}
}
_, err := c.groupSvc.SetLanguageTag(ctx, group, i.GuildID, languageTag)
if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
successMessageID := "cmd.group.set.language.success"
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: successMsg,
},
})
}
func (c *groupCommand) handleSetChannelGains(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
locale := string(i.Locale)
@ -987,11 +1163,14 @@ func (c *groupCommand) handleSetBarbarians(ctx context.Context, s *discordgo.Ses
}
func (c *groupCommand) handleTribe(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
switch i.ApplicationCommandData().Options[0].Options[0].Name {
name := i.ApplicationCommandData().Options[0].Options[0].Name
switch name {
case "add":
c.handleTribeAdd(ctx, s, i)
case "remove":
c.handleTribeRemove(ctx, s, i)
default:
c.logger.Error("unknown subcommand", zap.String("command", "group tribe"), zap.String("subcommand", name))
}
}

View File

@ -30,3 +30,7 @@ func boolToEmoji(val bool) string {
}
return ":x:"
}
func buildLangMessageID(languageTag string) string {
return "lang." + languageTag
}

View File

@ -1,4 +1,7 @@
{
"lang.pl": "Polish",
"lang.en": "English",
"err.default": "Something went wrong. Try again later.",
"err.required": "{{ .Field }} can't be blank.",
"err.group-limit-reached": "The group limit has been reached ({{ .Current }}/{{ .Limit }}).",
@ -12,11 +15,14 @@
"err.tribe-not-found": "Tribe (tag={{ .Tag }}) not found.",
"err.tribe-does-not-exist": "The tribe (tag={{ .Tag }}) doesn't exist.",
"job.execute-monitors.embed.description": "{{ buildPlayerMarkdown .Ennoblement.NewOwner }} has taken {{ buildLink .Ennoblement.Village.FullName .Ennoblement.Village.ProfileURL }} (Old owner: {{ buildPlayerMarkdown .Ennoblement.Village.Player }}).",
"cmd.group.description": "Manages groups on this server",
"cmd.group.create.description": "Creates a new monitor group",
"cmd.group.create.option.version.description": "e.g. www.tribalwars.net, www.plemiona.pl",
"cmd.group.create.option.server.description": "Tribal Wars server (e.g. en115, pl170)",
"cmd.group.create.option.language.description": "Language in which you wish to receive notifications",
"cmd.group.create.option.internals.description": "Show conquers in the same group",
"cmd.group.create.option.barbarians.description": "Show barbarian conquers",
"cmd.group.create.option.channel-gains.description": "Channel where notifications of gained villages will appear",
@ -25,10 +31,10 @@
"cmd.group.list.description": "Lists all created groups on this server",
"cmd.group.list.embed.title": "Group list",
"cmd.group.list.embed.field.value": "**Server**: {{ .ServerKey }}\n**Channel gains**: {{ .ChannelGains }}\n**Channel losses**: {{ .ChannelLosses }}\n**Internals**: {{ .Internals }}\n **Barbarians**: {{ .Barbarians }}\n **Number of monitored tribes**: {{ .NumTribes }}",
"cmd.group.list.embed.field.value": "**Server**: {{ .ServerKey }}\n**Language**: {{ .Language }}\n**Channel gains**: {{ .ChannelGains }}\n**Channel losses**: {{ .ChannelLosses }}\n**Internals**: {{ .Internals }}\n **Barbarians**: {{ .Barbarians }}\n **Number of monitored tribes**: {{ .NumTribes }}",
"cmd.group.details.description": "Displays group details (including added tribes)",
"cmd.group.details.embed.description": "**Server**: {{ .ServerKey }}\n**Channel gains**: {{ .ChannelGains }}\n**Channel losses**: {{ .ChannelLosses }}\n**Internals**: {{ .Internals }}\n **Barbarians**: {{ .Barbarians }}\n **Tribes**: {{ .Tribes }}",
"cmd.group.details.embed.description": "**Server**: {{ .ServerKey }}\n**Language**: {{ .Language }}\n**Channel gains**: {{ .ChannelGains }}\n**Channel losses**: {{ .ChannelLosses }}\n**Internals**: {{ .Internals }}\n **Barbarians**: {{ .Barbarians }}\n **Tribes**: {{ .Tribes }}",
"cmd.group.tribe.description": "Manages tribes in a group",
@ -44,6 +50,11 @@
"cmd.group.set.description": "Sets various properties in group configuration",
"cmd.group.set.language.description": "Sets the language in which you will receive notifications",
"cmd.group.set.language.option.group.description": "Group ID",
"cmd.group.set.language.option.language.description": "Language in which you wish to receive notifications",
"cmd.group.set.language.success": "The group has been successfully updated.",
"cmd.group.set.channel-gains.description": "Enables/disables notifications of gained villages",
"cmd.group.set.channel-gains.option.group.description": "Group ID",
"cmd.group.set.channel-gains.option.channel.description": "Channel where notifications of gained villages will appear",

View File

@ -1,4 +1,7 @@
{
"lang.pl": "Polski",
"lang.en": "Angielski",
"err.default": "Coś poszło nie tak. Spróbuj ponownie później.",
"err.required": "{{ .Field }} nie może być pusty.",
"err.group-limit-reached": "Limit grup został osiągnięty ({{ .Current }}/{{ .Limit }}).",
@ -12,11 +15,14 @@
"err.tribe-not-found": "Plemię (skrót={{ .Tag }}) nie zostało znalezione.",
"err.tribe-does-not-exist": "Plemię (skrót={{ .Tag }}) nie istnieje.",
"job.execute-monitors.embed.description": "{{ buildPlayerMarkdown .Ennoblement.NewOwner }} przejął {{ buildLink .Ennoblement.Village.FullName .Ennoblement.Village.ProfileURL }} (Poprzedni właściciel: {{ buildPlayerMarkdown .Ennoblement.Village.Player }}).",
"cmd.group.description": "Umożliwia zarządzanie grupami na tym serwerze",
"cmd.group.create.description": "Tworzy nową grupę",
"cmd.group.create.option.version.description": "np. www.tribalwars.net, www.plemiona.pl",
"cmd.group.create.option.server.description": "Serwer (np. en115, pl170)",
"cmd.group.create.option.language.description": "Język, w którym mają być wysyłane powiadomienia",
"cmd.group.create.option.internals.description": "Informuj o przejęciach wewnętrznych",
"cmd.group.create.option.barbarians.description": "Informuj o przejęciach wiosek barbarzyńskich",
"cmd.group.create.option.channel-gains.description": "Kanał na którym będą pojawiać się powiadomienia o zdobytych wioskach",
@ -25,10 +31,10 @@
"cmd.group.list.description": "Wyświetla wszystkie utworzone grupy na tym serwerze",
"cmd.group.list.embed.title": "Grupy",
"cmd.group.list.embed.field.value": "**Serwer**: {{ .ServerKey }}\n**Kanał zdobyte wioski**: {{ .ChannelGains }}\n**Kanał stracone wioski**: {{ .ChannelLosses }}\n**Przejęcia wewnętrzne**: {{ .Internals }}\n **Przejęcia barbarek**: {{ .Barbarians }}\n **Liczba monitorowanych plemion**: {{ .NumTribes }}",
"cmd.group.list.embed.field.value": "**Serwer**: {{ .ServerKey }}\n**Język**: {{ .Language }}\n**Kanał zdobyte wioski**: {{ .ChannelGains }}\n**Kanał stracone wioski**: {{ .ChannelLosses }}\n**Przejęcia wewnętrzne**: {{ .Internals }}\n **Przejęcia barbarek**: {{ .Barbarians }}\n **Liczba monitorowanych plemion**: {{ .NumTribes }}",
"cmd.group.details.description": "Wyświetla szczegóły grupy (w tym dodane plemiona)",
"cmd.group.details.embed.description": "**Serwer**: {{ .ServerKey }}\n**Kanał zdobyte wioski**: {{ .ChannelGains }}\n**Kanał stracone wioski**: {{ .ChannelLosses }}\n**Przejęcia wewnętrzne**: {{ .Internals }}\n **Przejęcia barbarek**: {{ .Barbarians }}\n **Plemiona**: {{ .Tribes }}",
"cmd.group.details.embed.description": "**Serwer**: {{ .ServerKey }}\n**Język**: {{ .Language }}\n**Kanał zdobyte wioski**: {{ .ChannelGains }}\n**Kanał stracone wioski**: {{ .ChannelLosses }}\n**Przejęcia wewnętrzne**: {{ .Internals }}\n **Przejęcia barbarek**: {{ .Barbarians }}\n **Plemiona**: {{ .Tribes }}",
"cmd.group.tribe.description": "Zarządza plemionami w grupie",
@ -44,6 +50,11 @@
"cmd.group.set.description": "Ustawia różne właściwości w konfiguracji grupy",
"cmd.group.set.language.description": "Ustawia język w którym będziesz otrzymywać powiadomienia",
"cmd.group.set.language.option.group.description": "ID grupy",
"cmd.group.set.language.option.language.description": "Język, w którym mają być wysyłane powiadomienia",
"cmd.group.set.language.success": "Grupa została pomyślnie zaaktualizowana.",
"cmd.group.set.channel-gains.description": "Włącza/wyłącza powiadomienia o podbitych wioskach",
"cmd.group.set.channel-gains.option.group.description": "ID grupy",
"cmd.group.set.channel-gains.option.channel.description": "Kanał na którym będą pojawiać się powiadomienia o zdobytych wioskach",

View File

@ -112,3 +112,7 @@ func (l *Localizer) LocalizeError(lang string, errToLocalize error) string {
}
return msg
}
func (l *Localizer) LanguageTags() []language.Tag {
return l.bundle.LanguageTags()
}

View File

@ -2,12 +2,14 @@ package discord
import (
"context"
"fmt"
"sync"
"text/template"
"time"
"gitea.dwysokinski.me/twhelp/dcbot/internal/discord/internal/discordi18n"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/bwmarrin/discordgo"
"github.com/nicksnyder/go-i18n/v2/i18n"
"go.uber.org/zap"
)
@ -18,9 +20,10 @@ const (
)
type executeMonitorsJob struct {
s *discordgo.Session
svc GroupService
logger *zap.Logger
s *discordgo.Session
svc GroupService
localizer *discordi18n.Localizer
logger *zap.Logger
}
func (e *executeMonitorsJob) Run() {
@ -30,19 +33,32 @@ func (e *executeMonitorsJob) Run() {
start := time.Now()
notifications, err := e.svc.Execute(ctx)
if err != nil {
e.logger.Error("something went wrong while executing monitors", zap.Error(err))
e.logger.Error(
"something went wrong while executing monitors",
zap.Error(err),
zap.Duration("duration", time.Since(start)),
)
return
}
convertedNotifications, err := (ennoblementNotificationToMessageEmbedConverter{notifications, e.localizer}).convert()
if err != nil {
e.logger.Error("couldn't convert []domain.EnnoblementNotification to []*discordgo.MessageEmbed", zap.Error(err))
return
}
var wg sync.WaitGroup
for ch, embeds := range (ennoblementNotificationToMessageEmbedConverter{notifications}).convert() {
for ch, embeds := range convertedNotifications {
wg.Add(1)
go func(ch string, embeds []*discordgo.MessageEmbed) {
defer wg.Done()
for _, embed := range embeds {
_, _ = e.s.ChannelMessageSendEmbed(ch, embed)
_, sendErr := e.s.ChannelMessageSendEmbed(ch, embed)
if sendErr != nil {
e.logger.Warn("couldn't send notification", zap.String("channel", ch), zap.Error(sendErr))
}
}
}(ch, embeds)
}
@ -57,10 +73,11 @@ func (e *executeMonitorsJob) Run() {
}
type ennoblementNotificationToMessageEmbedConverter struct {
ns []domain.EnnoblementNotification
ns []domain.EnnoblementNotification
localizer *discordi18n.Localizer
}
func (c ennoblementNotificationToMessageEmbedConverter) convert() map[string][]*discordgo.MessageEmbed {
func (c ennoblementNotificationToMessageEmbedConverter) convert() (map[string][]*discordgo.MessageEmbed, error) {
m := make(map[string]map[domain.EnnoblementNotificationType][]*discordgo.MessageEmbed)
timestamp := formatTimestamp(time.Now())
for _, n := range c.ns {
@ -70,8 +87,11 @@ func (c ennoblementNotificationToMessageEmbedConverter) convert() map[string][]*
embeds := m[n.ChannelID][n.Type]
str := c.buildDescription(n)
if l := len(embeds); l == 0 || len(embeds[l-1].Description)+len(str) > embedDescriptionCharLimit {
description, err := c.buildDescription(n)
if err != nil {
return nil, err
}
if l := len(embeds); l == 0 || len(embeds[l-1].Description)+len(description) > embedDescriptionCharLimit {
embeds = append(embeds, &discordgo.MessageEmbed{
Color: c.mapNotificationTypeToColor(n.Type),
Type: discordgo.EmbedTypeRich,
@ -79,9 +99,9 @@ func (c ennoblementNotificationToMessageEmbedConverter) convert() map[string][]*
})
}
if len(embeds[len(embeds)-1].Description) > 0 {
str = "\n" + str
description = "\n" + description
}
embeds[len(embeds)-1].Description += str
embeds[len(embeds)-1].Description += description
m[n.ChannelID][n.Type] = embeds
}
@ -99,16 +119,20 @@ func (c ennoblementNotificationToMessageEmbedConverter) convert() map[string][]*
}
}
return res
return res, nil
}
func (c ennoblementNotificationToMessageEmbedConverter) buildDescription(n domain.EnnoblementNotification) string {
return fmt.Sprintf(
"%s has taken %s (Old owner: %s)",
c.buildPlayerMarkdown(n.Ennoblement.NewOwner),
buildLink(n.Ennoblement.Village.FullName, n.Ennoblement.Village.ProfileURL),
c.buildPlayerMarkdown(n.Ennoblement.Village.Player),
)
func (c ennoblementNotificationToMessageEmbedConverter) buildDescription(n domain.EnnoblementNotification) (string, error) {
return c.localizer.Localize(n.LanguageTag, &i18n.LocalizeConfig{
MessageID: "job.execute-monitors.embed.description",
TemplateData: map[string]any{
"Ennoblement": n.Ennoblement,
},
Funcs: template.FuncMap{
"buildPlayerMarkdown": c.buildPlayerMarkdown,
"buildLink": buildLink,
},
})
}
func (c ennoblementNotificationToMessageEmbedConverter) buildPlayerMarkdown(p domain.NullPlayerMeta) string {

View File

@ -11,5 +11,6 @@ type EnnoblementNotification struct {
Type EnnoblementNotificationType
ServerID string
ChannelID string
LanguageTag string
Ennoblement Ennoblement
}

View File

@ -14,6 +14,7 @@ type Group struct {
Barbarians bool // Show barbarian conquers
ServerKey string // TW server key (e.g. en130)
VersionCode string
LanguageTag string // BCP 47 language tag
CreatedAt time.Time
}
@ -37,10 +38,11 @@ type CreateGroupParams struct {
channelLosses string
barbarians bool
internals bool
languageTag string
}
func NewCreateGroupParams(
serverID, versionCode, serverKey, channelGains, channelLosses string,
serverID, versionCode, serverKey, languageTag, channelGains, channelLosses string,
barbarians, internals bool,
) (CreateGroupParams, error) {
if serverID == "" {
@ -55,6 +57,10 @@ func NewCreateGroupParams(
return CreateGroupParams{}, RequiredError{Field: "ServerKey"}
}
if languageTag == "" {
return CreateGroupParams{}, RequiredError{Field: "LanguageTag"}
}
return CreateGroupParams{
serverID: serverID,
serverKey: serverKey,
@ -63,6 +69,7 @@ func NewCreateGroupParams(
channelLosses: channelLosses,
barbarians: barbarians,
internals: internals,
languageTag: languageTag,
}, nil
}
@ -74,6 +81,10 @@ func (c CreateGroupParams) VersionCode() string {
return c.versionCode
}
func (c CreateGroupParams) LanguageTag() string {
return c.languageTag
}
func (c CreateGroupParams) ServerKey() string {
return c.serverKey
}
@ -97,6 +108,7 @@ func (c CreateGroupParams) Internals() bool {
type UpdateGroupParams struct {
ChannelGains NullString
ChannelLosses NullString
LanguageTag NullString
Internals NullBool
Barbarians NullBool
}
@ -105,7 +117,8 @@ func (u UpdateGroupParams) IsZero() bool {
return !u.ChannelGains.Valid &&
!u.ChannelLosses.Valid &&
!u.Internals.Valid &&
!u.Barbarians.Valid
!u.Barbarians.Valid &&
!u.LanguageTag.Valid
}
type ListGroupsParams struct {

View File

@ -5,6 +5,7 @@ import (
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
)
func TestNewCreateGroupParams(t *testing.T) {
@ -15,6 +16,7 @@ func TestNewCreateGroupParams(t *testing.T) {
serverID string
versionCode string
serverKey string
languageTag string
channelGains string
channelLosses string
barbarians bool
@ -26,6 +28,7 @@ func TestNewCreateGroupParams(t *testing.T) {
serverID: "123441",
versionCode: "en",
serverKey: "en113",
languageTag: language.English.String(),
channelGains: "1234",
channelLosses: "1234",
barbarians: true,
@ -51,6 +54,14 @@ func TestNewCreateGroupParams(t *testing.T) {
serverKey: "",
err: domain.RequiredError{Field: "ServerKey"},
},
{
name: "ERR: LanguageTag cannot be blank",
serverID: "1234",
versionCode: "en",
serverKey: "en113",
languageTag: "",
err: domain.RequiredError{Field: "LanguageTag"},
},
}
for _, tt := range tests {
@ -63,6 +74,7 @@ func TestNewCreateGroupParams(t *testing.T) {
tt.serverID,
tt.versionCode,
tt.serverKey,
tt.languageTag,
tt.channelGains,
tt.channelLosses,
tt.barbarians,
@ -93,28 +105,6 @@ func TestUpdateGroupParams_IsZero(t *testing.T) {
params domain.UpdateGroupParams
output bool
}{
{
name: "OK: all",
params: domain.UpdateGroupParams{
ChannelGains: domain.NullString{
String: "123",
Valid: true,
},
ChannelLosses: domain.NullString{
String: "123",
Valid: true,
},
Barbarians: domain.NullBool{
Bool: false,
Valid: true,
},
Internals: domain.NullBool{
Bool: false,
Valid: true,
},
},
output: false,
},
{
name: "OK: ChannelGains",
params: domain.UpdateGroupParams{
@ -155,6 +145,16 @@ func TestUpdateGroupParams_IsZero(t *testing.T) {
},
output: false,
},
{
name: "OK: LanguageTag",
params: domain.UpdateGroupParams{
LanguageTag: domain.NullString{
String: language.English.String(),
Valid: true,
},
},
output: false,
},
{
name: "OK: empty struct",
params: domain.UpdateGroupParams{},

View File

@ -27,6 +27,7 @@ func (b ennoblementNotificationBuilder) buildGroup(g domain.GroupWithMonitors) [
Type: domain.EnnoblementNotificationTypeGain,
ServerID: g.ServerID,
ChannelID: g.ChannelGains,
LanguageTag: g.LanguageTag,
Ennoblement: b.ennoblementToDomainModel(e),
})
}
@ -36,6 +37,7 @@ func (b ennoblementNotificationBuilder) buildGroup(g domain.GroupWithMonitors) [
Type: domain.EnnoblementNotificationTypeLoss,
ServerID: g.ServerID,
ChannelID: g.ChannelLosses,
LanguageTag: g.LanguageTag,
Ennoblement: b.ennoblementToDomainModel(e),
})
}

View File

@ -209,6 +209,15 @@ func (g *Group) SetBarbarians(ctx context.Context, id, serverID string, barbaria
})
}
func (g *Group) SetLanguageTag(ctx context.Context, id, serverID, languageTag string) (domain.GroupWithMonitors, error) {
return g.update(ctx, id, serverID, domain.UpdateGroupParams{
LanguageTag: domain.NullString{
String: languageTag,
Valid: true,
},
})
}
func (g *Group) update(ctx context.Context, id, serverID string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error) {
// check if group exists
if _, err := g.Get(ctx, id, serverID); err != nil {

View File

@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"golang.org/x/text/language"
)
func TestGroup_Create(t *testing.T) {
@ -26,6 +27,7 @@ func TestGroup_Create(t *testing.T) {
"592292203234328587",
"en",
"en113",
language.English.String(),
"1234",
"1235",
true,
@ -457,6 +459,90 @@ func TestGroup_RemoveTribe(t *testing.T) {
})
}
func TestGroup_SetLanguageTag(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
repo := &mock.FakeGroupRepository{}
group := domain.GroupWithMonitors{
Group: domain.Group{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
},
}
repo.GetReturns(group, nil)
repo.UpdateCalls(func(ctx context.Context, id string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error) {
return domain.GroupWithMonitors{
Group: domain.Group{
ID: id,
LanguageTag: params.LanguageTag.String,
},
}, nil
})
languageTag := uuid.NewString()
g, err := service.
NewGroup(repo, nil, zap.NewNop(), 1, 1).
SetLanguageTag(context.Background(), group.ID, group.ServerID, languageTag)
assert.NoError(t, err)
assert.Equal(t, group.ID, g.ID)
assert.Equal(t, languageTag, g.LanguageTag)
})
t.Run("ERR: group not found", func(t *testing.T) {
t.Parallel()
group := domain.GroupWithMonitors{
Group: domain.Group{
ID: uuid.NewString(),
ServerID: uuid.NewString(),
},
}
tests := []struct {
name string
group domain.GroupWithMonitors
serverID string
err error
}{
{
name: "repository returned error",
serverID: group.ServerID,
err: domain.GroupNotFoundError{
ID: group.ID,
},
},
{
name: "incorrect server id",
group: group,
serverID: uuid.NewString(),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
repo := &mock.FakeGroupRepository{}
repo.GetReturns(tt.group, tt.err)
g, err := service.
NewGroup(repo, nil, zap.NewNop(), 1, 1).
SetLanguageTag(context.Background(), group.ID, tt.serverID, uuid.NewString())
assert.ErrorIs(t, err, domain.GroupNotFoundError{
ID: group.ID,
})
assert.Zero(t, g)
})
}
})
}
func TestGroup_SetChannelGains(t *testing.T) {
t.Parallel()

View File

@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE groups ADD COLUMN language_tag varchar(10) NOT NULL DEFAULT 'en'; -- BCP 47 language tag
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE groups DROP COLUMN IF EXISTS language_tag;
-- +goose StatementEnd