diff --git a/.gitignore b/.gitignore index a759b21..e7e29ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -tokens/*token \ No newline at end of file +tokens/*token +backups/* \ No newline at end of file diff --git a/main.go b/main.go index 2196859..5b7b8e0 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "galched-bot/modules/discord" "galched-bot/modules/grace" "galched-bot/modules/settings" + "galched-bot/modules/subday" "go.uber.org/fx" ) @@ -25,14 +26,16 @@ type ( func (s *silentPrinter) Printf(str string, i ...interface{}) {} -func start(p appParam) { +func start(p appParam) error { var err error log.Print("main: starting galched-bot v", p.Settings.Version) err = p.Discord.Start() if err != nil { - log.Fatal("discord: cannot start instance", err) + log.Print("discord: cannot start instance", err) + return err + } log.Printf("main: discord instance running") log.Printf("main: — — —") @@ -42,17 +45,19 @@ func start(p appParam) { err = p.Discord.Stop() if err != nil { - log.Fatal("discord: cannot stop instance", err) + log.Print("discord: cannot stop instance", err) + return err } log.Print("main: galched bot successfully stopped") + return nil } func main() { var err error app := fx.New( fx.Logger(new(silentPrinter)), - fx.Provide(settings.New, grace.New, discord.New), + fx.Provide(settings.New, grace.New, discord.New, subday.New), fx.Invoke(start)) err = app.Start(context.Background()) diff --git a/modules/discord/discord.go b/modules/discord/discord.go index 3a19213..37a40e0 100644 --- a/modules/discord/discord.go +++ b/modules/discord/discord.go @@ -5,6 +5,7 @@ import ( "log" "galched-bot/modules/settings" + "galched-bot/modules/subday" "github.com/bwmarrin/discordgo" "github.com/pkg/errors" @@ -18,7 +19,7 @@ type ( } ) -func New(s *settings.Settings) (*Discord, error) { +func New(s *settings.Settings, subday *subday.Subday) (*Discord, error) { key := fmt.Sprintf("Bot %s", s.DiscordToken) instance, err := discordgo.New(key) if err != nil { @@ -26,6 +27,9 @@ func New(s *settings.Settings) (*Discord, error) { } processor := NewProcessor(s.Version) + for _, subdayHandler := range SubdayHandlers(subday, s.PermittedRoles) { + processor.AddHandler(subdayHandler) + } log.Printf("discord: added %d message handlers", len(processor.handlers)) if len(processor.handlers) > 0 { @@ -45,6 +49,13 @@ func LogMessage(m *discordgo.MessageCreate) { log.Printf("discord: msg [%s]: %s", m.Author.Username, m.Content) } +func SendMessage(s *discordgo.Session, m *discordgo.MessageCreate, text string) { + _, err := s.ChannelMessageSend(m.ChannelID, text) + if err != nil { + log.Printf("discord: cannot send message [%s]: %v", text, err) + } +} + func (d *Discord) Start() error { d.session.AddHandler(d.processor.Process) return d.session.Open() diff --git a/modules/discord/subdayhandlers.go b/modules/discord/subdayhandlers.go new file mode 100644 index 0000000..c481f3e --- /dev/null +++ b/modules/discord/subdayhandlers.go @@ -0,0 +1,150 @@ +package discord + +import ( + "fmt" + "log" + "strings" + + "galched-bot/modules/subday" + + "github.com/bwmarrin/discordgo" +) + +type SubdayListHandler struct { + subday *subday.Subday +} + +func (h *SubdayListHandler) Signature() string { + return "!sublist" +} +func (h *SubdayListHandler) Description() string { + return "список игр для сабдея" +} +func (h *SubdayListHandler) IsValid(msg string) bool { + return msg == "!sublist" +} +func (h *SubdayListHandler) Handle(s *discordgo.Session, m *discordgo.MessageCreate) { + h.subday.RLock() + defer h.subday.RUnlock() + LogMessage(m) + + c, err := s.State.Channel(m.ChannelID) + if err != nil { + log.Print("discord: cannot obtain state", err) + return + } + g, err := s.State.Guild(c.GuildID) + if err != nil { + log.Print("discord: cannot obtain guild", err) + return + } + message := "Игры предыдущих сабдеев:\n**20.10.18**: _DmC_ -> _Fable 1_ -> _Overcooked 2_\n" + + "**17.11.18**: _The Witcher_ -> _Xenus: Белое Золото_ -> _NFS: Underground 2_\n" + + "**22.12.18**: _True Crime: Streets of LA_ -> _Serious Sam 3_ -> _Kholat_\n" + + "**26.01.19**: _Disney’s Aladdin_ -> _~~Gothic~~_ -> _Scrapland_ -> _Donut County_\n\n" + + "Список игр для следующего сабдея:\n" + for k, v := range h.subday.Database() { + nickname := " " + for _, member := range g.Members { + if k == member.User.ID { + if member.Nick != "" { + nickname = member.Nick + } else { + nickname = member.User.Username + } + } + } + for _, game := range v { + message += fmt.Sprintf(" **- %s** от _%s_\n", game, nickname) + } + } + _, err = s.ChannelMessageSend(m.ChannelID, strings.Trim(message, "\n")) + if err != nil { + log.Printf("discord: cannot send message [%s]: %v", message, err) + } +} + +type SubdayAddHandler struct { + subday *subday.Subday + roles []string +} + +func (h *SubdayAddHandler) Signature() string { + return "!subday " +} +func (h *SubdayAddHandler) Description() string { + return "добавление игры в список сабдея" +} +func (h *SubdayAddHandler) IsValid(msg string) bool { + return strings.HasPrefix(msg, "!subday") +} +func (h *SubdayAddHandler) Handle(s *discordgo.Session, m *discordgo.MessageCreate) { + h.subday.Lock() + defer h.subday.Unlock() + LogMessage(m) + + c, err := s.State.Channel(m.ChannelID) + if err != nil { + log.Print("discord: cannot obtain state", err) + return + } + g, err := s.State.Guild(c.GuildID) + if err != nil { + log.Print("discord: cannot obtain guild", err) + return + } + member, err := s.State.Member(g.ID, m.Author.ID) + if err != nil { + log.Print("discord: cannot obtain user role", err) + return + } + + if g.Name != "AV" && g.Name != "Galched" { + log.Printf("discord: message from unsupported guild %s, ignore", g.Name) + return + } + + permissionGranted := false +loop: + for i := range member.Roles { + for j := range h.roles { + if member.Roles[i] == h.roles[j] { + permissionGranted = true + break loop + } + } + } + + if permissionGranted { + game := strings.Trim(strings.Replace(m.Content, "!subday", "", 1), " ") + if game != "" { + gameList, ok := h.subday.Database()[m.Author.ID] + if ok && len(gameList) > 10 { + SendMessage(s, m, "Нельзя заказать больше 10 игр") + return + } else if ok { + for i := range gameList { + if game == gameList[i] { + SendMessage(s, m, "Эта игра уже заказана вами") + return + } + } + } + h.subday.Database()[m.Author.ID] = append(h.subday.Database()[m.Author.ID], game) + log.Printf("subday: game [%s] is added to subday database", game) + SendMessage(s, m, fmt.Sprintf("Игра \"%s\" добавлена в список", game)) + h.subday.DumpToFile() + } + } else { + log.Print("subday: game is not added, insufficient rights") + SendMessage(s, m, "Заказ игр для сабдея доступен только для подписчиков канала (и Нифлая)") + } +} + +func SubdayHandlers(s *subday.Subday, r []string) []MessageHandler { + var result []MessageHandler + + addHandler := &SubdayAddHandler{s, r} + listHandler := &SubdayListHandler{s} + return append(result, addHandler, listHandler) +} diff --git a/modules/discord/testhandler.go b/modules/discord/testhandler.go index 8ed4370..e98d8cd 100644 --- a/modules/discord/testhandler.go +++ b/modules/discord/testhandler.go @@ -6,20 +6,20 @@ import ( type testHandler struct{} -var _ = (testHandler)(nil) // ignore unused warning +var _ = testHandler{} // ignore unused warning -func (t *testHandler) Signature() string { +func (h *testHandler) Signature() string { return "!test" } -func (t *testHandler) Description() string { +func (h *testHandler) Description() string { return "тестовый хэндлер" } -func (t *testHandler) IsValid(msg string) bool { +func (h *testHandler) IsValid(msg string) bool { return msg == "!test" } -func (t *testHandler) Handle(s *discordgo.Session, m *discordgo.MessageCreate) { +func (h *testHandler) Handle(s *discordgo.Session, m *discordgo.MessageCreate) { LogMessage(m) } diff --git a/modules/settings/settings.go b/modules/settings/settings.go index 0847de5..5b3e139 100644 --- a/modules/settings/settings.go +++ b/modules/settings/settings.go @@ -3,32 +3,43 @@ package settings import ( "io/ioutil" "log" - "os" - - "github.com/pkg/errors" + "time" ) const ( - version = "3.0.0" - discordTokenPath = "./tokens/.discordtoken" + version = "3.0.0" + discordTokenPath = "./tokens/.discordtoken" + subdayDataPath = "./backups/subday" + subdayDataDuration = 10 // in seconds + + // Permitted roles in discord for subday + subRole1 = "433672344737677322" + subRole2 = "433680494635515904" + galchedRole = "301467455497175041" + smorcRole = "301470784491356172" ) type ( Settings struct { - Version string - DiscordToken string + Version string + DiscordToken string + SubdayDataPath string + SubdayJobDuration time.Duration + PermittedRoles []string } ) func New() (*Settings, error) { - log.Print(os.Getwd()) discordToken, err := ioutil.ReadFile(discordTokenPath) if err != nil { - return nil, errors.Wrap(err, "cannot read discord token file") + log.Print("settings: cannot read discord token file", err) } return &Settings{ - Version: version, - DiscordToken: string(discordToken), + Version: version, + DiscordToken: string(discordToken), + SubdayDataPath: subdayDataPath, + SubdayJobDuration: subdayDataDuration * time.Second, + PermittedRoles: []string{subRole1, subRole2, galchedRole, smorcRole}, }, nil } diff --git a/modules/subday/subday.go b/modules/subday/subday.go new file mode 100644 index 0000000..ae2b6cf --- /dev/null +++ b/modules/subday/subday.go @@ -0,0 +1,73 @@ +package subday + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "sync" + + "galched-bot/modules/settings" +) + +type Subday struct { + sync.RWMutex + path string + database map[string][]string +} + +func New(s *settings.Settings) (*Subday, error) { + var ( + err error + data = make(map[string][]string) + ) + + subdayData, err := ioutil.ReadFile(s.SubdayDataPath) + if err != nil { + log.Print("subday: cannot read subday data file", err) + log.Print("subday: creating new subday database") + } else { + err = json.Unmarshal(subdayData, &data) + if err != nil { + data = make(map[string][]string) + log.Print("subday: cannot unmarshal subday data file", err) + log.Print("subday: creating new subday database") + } else { + log.Print("subday: using previously saved subday database") + } + } + + subday := &Subday{ + RWMutex: sync.RWMutex{}, + path: s.SubdayDataPath, + database: data, + } + return subday, nil +} + +func (s *Subday) Database() map[string][]string { + return s.database +} + +func (s *Subday) DumpToFile() { + data, err := json.Marshal(s.database) + if err != nil { + log.Print("subday: cannot marshal database file", err) + return + } + file, err := os.Create(s.path) + if err != nil { + log.Print("subday: cannot open database file", err) + return + } + _, err = fmt.Fprintf(file, string(data)) + if err != nil { + log.Print("subday: cannot write to database file") + } + err = file.Close() + if err != nil { + log.Print("subday: cannot close database file") + } + log.Print("subday: database dumped to file") +}