diff --git a/bot.go b/bot.go deleted file mode 100644 index b4fe736..0000000 --- a/bot.go +++ /dev/null @@ -1,156 +0,0 @@ -package main - -import ( - "fmt" - "html/template" - "strconv" - "strings" - "time" - - "github.com/disgoorg/disgo/bot" - "github.com/disgoorg/disgo/discord" - "github.com/disgoorg/disgo/rest" - "github.com/disgoorg/snowflake/v2" -) - -type Bot struct { - Client bot.Client -} - -// Get a user's avatar URL. -func (b *Bot) GetAvatarURL(user discord.User) string { - return user.EffectiveAvatarURL() -} - -// get the fourum channels in a guild. -func (b *Bot) GetForums(guildID snowflake.ID) []discord.GuildForumChannel { - var forums []discord.GuildForumChannel - b.Client.Caches().Channels().ForEach(func(channel discord.Channel) { - guildForumChannel, ok := channel.(discord.GuildForumChannel) - if !ok || guildForumChannel.GuildID() != guildID { - return - } - forums = append(forums, guildForumChannel) - }) - return forums -} - -// Get the title of a guild. -func (b *Bot) GetGuildName(guildID snowflake.ID) string { - guild, ok := b.Client.Caches().Guilds().Get(guildID) - if !ok { - return "unknown" - } - return guild.Name -} - -// Get the title of a channel -func (b *Bot) GetChannelTitle(channelID snowflake.ID) string { - channel, ok := b.Client.Caches().Channels().Get(channelID) - if !ok { - return "unknown" - } - return channel.Name() -} - -// Get a channel's threads. -func (b *Bot) GetThreadsInChannel(channelID snowflake.ID) []discord.GuildThread { - channels := b.Client.Caches().Channels().GuildThreadsInChannel(channelID) - - // archived threads aren't cached so we need to get those and add them - threadsObj := rest.NewThreads(b.Client.Rest()) - archivedChannels, err := threadsObj.GetPublicArchivedThreads(channelID, time.Now(), 100) - // todo: handle the error more properly - if err == nil { - channels = append(channels, archivedChannels.Threads...) - } else { - fmt.Println(err) - } - - return channels -} - -type Messages struct { - Messages []discord.Message - Error error -} - -// Get the last 100 messages in a channel. -func (b *Bot) GetMessagesInChannel(channelID snowflake.ID) (messages Messages) { - done := false - before := snowflake.ID(0) - for !done { - // get the past 100 messages in a channel. - msgs, err := b.Client.Rest().GetMessages(channelID, 100, before, 0, 0) - if err != nil { - messages.Error = err - return - } - for _, message := range msgs { - messages.Messages = append([]discord.Message{message}, messages.Messages...) - } - // If we were only able to get up to 100 messages, - // search again but after the message we stopped at. - if len(msgs) >= 100 { - before = msgs[99].ID - } else { - done = true - } - } - return -} - -// Get a channel unless it's not one we should be able to view, -func (b *Bot) GetChannel(channelID snowflake.ID) (discord.Channel, error) { - channel, ok := b.Client.Caches().Channels().Get(channelID) - if !ok { - return nil, fmt.Errorf("channel not found") - } - if channel.Type() != discord.ChannelTypeGuildForum && channel.Type() != discord.ChannelTypeGuildPublicThread { - return nil, fmt.Errorf("channel is not a forum") - } - return channel, nil -} - -// Count all the messages in all the threads of a channel. -func (b *Bot) PostCount(channelID snowflake.ID) string { - threads := b.GetThreadsInChannel(channelID) - count := 0 - for _, v := range threads { - messages := b.GetMessagesInChannel(v.ID()) - count += len(messages.Messages) - } - return fmt.Sprint(count) -} - -// Analyze content for mentions and emojis -func (b *Bot) FormatDiscordThings(guildID snowflake.ID, content string) template.HTML { - parts := strings.Split(content, " ") - var newContent string - for _, v := range parts { - switch { - /*case mentionRe.Match([]byte(v)): - newContent += FormatUserMention(guildID, template.HTMLEscapeString(v)) + " " - /*case emojiRe.Match(v): - newContent += FormatEmojiMention(guildID, template.HTMLEscapeString(v)) + " "*/ - default: - newContent += replacer.Replace(template.HTMLEscapeString(v)) + " " - } - } - return template.HTML(Markdown(newContent)) -} - -func (b *Bot) FormatUserMention(guildID snowflake.ID, userID snowflake.ID) string { - var memberID string - numOnlyRe.ReplaceAllString(memberID, userID.String()) - member, ok := b.Client.Caches().Members().Get(guildID, userID) - if !ok { - return fmt.Sprintf("<@%d>", userID) - } - return "@" + member.EffectiveName() -} - -// Get the number of guilds we're in. -func (b *Bot) GuildNum() string { - return strconv.Itoa(b.Client.Caches().Guilds().Len()) -} diff --git a/config.go b/config.go deleted file mode 100644 index a54f012..0000000 --- a/config.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/pelletier/go-toml/v2" -) - -var LocalConfig struct { - BotToken string -} - -func init() { - file, err := os.ReadFile("config.toml") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - toml.Unmarshal(file, &LocalConfig) -} diff --git a/discord.go b/discord.go deleted file mode 100644 index a07df94..0000000 --- a/discord.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "context" - "fmt" - "regexp" - "strings" - - "github.com/disgoorg/disgo" - "github.com/disgoorg/disgo/bot" - "github.com/disgoorg/disgo/cache" - "github.com/disgoorg/disgo/events" - "github.com/disgoorg/disgo/gateway" - "github.com/disgoorg/log" -) - -// things that need to be replaced with html or otherwise - -var replacer = strings.NewReplacer( - "\n", "
", - "<", "<", - ">", ">", -) - -// regexp checks -var mentionRe = regexp.MustCompile(`<@([0-9]*)>`) -var emojiRe = regexp.MustCompile(`<:([A-z]*?):([0-9]*)>`) -var numOnlyRe = regexp.MustCompile(`([^0-9])`) - -func InitBot() *Bot { - log.SetLevel(log.LevelDebug) - client, err := disgo.New(LocalConfig.BotToken, - bot.WithGatewayConfigOpts( - gateway.WithIntents(gateway.IntentsNonPrivileged, gateway.IntentMessageContent), - ), - bot.WithCacheConfigOpts( - cache.WithCacheFlags(cache.FlagGuilds, cache.FlagChannels, cache.FlagMembers, cache.FlagEmojis, cache.FlagStickers), - ), - bot.WithEventListenerFunc(func(e *events.Ready) { - selfUser, _ := e.Client().Caches().GetSelfUser() - fmt.Printf("Logged in as: %s\n", selfUser.Tag()) - }), - ) - if err != nil { - log.Fatalf("error while creating client: %s", err) - } - - if err = client.OpenGateway(context.Background()); err != nil { - log.Fatalf("error while connecting to discord: %s", err) - } - - return &Bot{ - Client: client, - } -} diff --git a/funcmap.go b/funcmap.go index f20fd87..4906fdd 100644 --- a/funcmap.go +++ b/funcmap.go @@ -1,23 +1,34 @@ package main import ( - "html/template" - - "github.com/disgoorg/snowflake/v2" + "fmt" + "time" ) -func FuncMap(b *Bot) template.FuncMap { - return template.FuncMap{ - "GetAvatarURL": b.GetAvatarURL, - "GetForums": b.GetForums, - "GetGuildName": b.GetGuildName, - "GetThreadsInChannel": b.GetThreadsInChannel, - "GetChannelTitle": b.GetChannelTitle, - "GetMessagesInChannel": b.GetMessagesInChannel, - "PostCount": b.PostCount, - "PrettyTime": PrettyTime, - "FormatDiscordThings": b.FormatDiscordThings, - "GuildNum": b.GuildNum, - "ParseSnowflake": snowflake.MustParse, +var funcMap = map[string]any{ + "PrettyTime": PrettyTime, +} + +func PrettyTime(timestamp time.Time) string { + unixTimeDur := time.Now().Sub(timestamp) + + if unixTimeDur.Hours() >= 8760 { + return fmt.Sprintf("%0.f years ago", unixTimeDur.Hours()/8760) + } + if unixTimeDur.Hours() >= 730 { + return fmt.Sprintf("%0.f months ago", unixTimeDur.Hours()/730) + } + if unixTimeDur.Hours() >= 168 { + return fmt.Sprintf("%0.f weeks ago", unixTimeDur.Hours()/168) + } + if unixTimeDur.Hours() >= 24 { + return fmt.Sprintf("%0.f days ago", unixTimeDur.Hours()/24) + } + if unixTimeDur.Hours() >= 1 { + return fmt.Sprintf("%0.f hours ago", unixTimeDur.Hours()) + } + if unixTimeDur.Minutes() >= 1 { + return fmt.Sprintf("%0.f minutes ago", unixTimeDur.Minutes()) } + return fmt.Sprintf("%0.f seconds ago", unixTimeDur.Seconds()) } diff --git a/go.mod b/go.mod index 350161a..a1c90f4 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,22 @@ module github.com/IoIxD/DFS go 1.19 require ( - github.com/disgoorg/disgo v0.13.21-0.20220916011522-16246893ae55 - github.com/disgoorg/log v1.2.0 - github.com/disgoorg/snowflake/v2 v2.0.0 - github.com/pelletier/go-toml/v2 v2.0.5 + github.com/diamondburned/ningen/v3 v3.0.0-20220619214735-56004aa62571 + github.com/naoina/toml v0.1.1 ) require ( - github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b // indirect - golang.org/x/exp v0.0.0-20220325121720-054d8573a5d8 // indirect + github.com/gorilla/schema v1.2.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/naoina/go-stringutil v0.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + go4.org v0.0.0-20200411211856-f5505b9728dd // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect ) require ( - github.com/gomarkdown/markdown v0.0.0-20220905174103-7b278df48cfb + github.com/diamondburned/arikawa/v3 v3.1.1-0.20220919215554-d96ce0f54cf1 + github.com/go-chi/chi/v5 v5.0.7 github.com/gorilla/websocket v1.5.0 // indirect + github.com/yuin/goldmark v1.4.14 ) diff --git a/go.sum b/go.sum index edc4069..d19c5aa 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,250 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/disgoorg/disgo v0.13.21-0.20220916011522-16246893ae55 h1:r055a655iK7aMvHWHja3qLRdKQrD4GiMKIKKFg4iP7Q= -github.com/disgoorg/disgo v0.13.21-0.20220916011522-16246893ae55/go.mod h1:Cyip4bCYHD3rHgDhBPT9cLo81e9AMbDe8ocM50UNRM4= -github.com/disgoorg/log v1.2.0 h1:sqlXnu/ZKAlIlHV9IO+dbMto7/hCQ474vlIdMWk8QKo= -github.com/disgoorg/log v1.2.0/go.mod h1:3x1KDG6DI1CE2pDwi3qlwT3wlXpeHW/5rVay+1qDqOo= -github.com/disgoorg/snowflake/v2 v2.0.0 h1:+xvyyDddXmXLHmiG8SZiQ3sdZdZPbUR22fSHoqwkrOA= -github.com/disgoorg/snowflake/v2 v2.0.0/go.mod h1:SPU9c2CNn5DSyb86QcKtdZgix9osEtKrHLW4rMhfLCs= -github.com/gomarkdown/markdown v0.0.0-20220905174103-7b278df48cfb h1:7h+tPfwoUE+qLvWYmsvKSiRlXv6WGorb6PUKaZUclwc= -github.com/gomarkdown/markdown v0.0.0-20220905174103-7b278df48cfb/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/diamondburned/ningen/v3 v3.0.0-20220619214735-56004aa62571 h1:puq+HgCOAlF5EOQsTLNNGS1wB6Dv7/KwP5uT1TuUSBA= +github.com/diamondburned/ningen/v3 v3.0.0-20220619214735-56004aa62571/go.mod h1:WLSUx3megnWk5I6rYLE+yPHN3iPJf4Nag2B5Y7l+Vmc= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8= +github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b h1:qYTY2tN72LhgDj2rtWG+LI6TXFl2ygFQQ4YezfVaGQE= -github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -golang.org/x/exp v0.0.0-20220325121720-054d8573a5d8 h1:Xt4/LzbTwfocTk9ZLEu4onjeFucl88iW+v4j4PWbQuE= -golang.org/x/exp v0.0.0-20220325121720-054d8573a5d8/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/twmb/murmur3 v1.1.3/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= +github.com/yuin/goldmark v1.3.2/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.14 h1:jwww1XQfhJN7Zm+/a1ZA/3WUiEBEroYFNTiV3dKwM8U= +github.com/yuin/goldmark v1.4.14/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU= +go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211001092434-39dca1131b70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/main.go b/main.go index 07985f9..606ed56 100644 --- a/main.go +++ b/main.go @@ -3,177 +3,96 @@ package main import ( "context" "embed" + "flag" "html/template" + "io/fs" "log" "net/http" - "net/url" "os" - "path/filepath" - "regexp" - "strings" + "os/signal" "time" + + "github.com/diamondburned/arikawa/v3/gateway" + "github.com/diamondburned/arikawa/v3/state" + "github.com/naoina/toml" ) -//go:embed pages/*.* -var pages embed.FS +//go:embed resources +var embedfs embed.FS var tmpl *template.Template -var NumbersOnlyRe *regexp.Regexp -var SitemapRe *regexp.Regexp - -var Client *Bot func main() { - // initialize the discord shit - Client = InitBot() - defer Client.Client.Close(context.TODO()) - - // initialize the template shit - tmpl = template.New("") - tmpl.Funcs(FuncMap(Client)) - _, err := tmpl.ParseFS(pages, "pages/*") + cfgpath := flag.String("config", "config.toml", "path to config.toml") + flag.Parse() + file, err := os.ReadFile(*cfgpath) if err != nil { - log.Println(err) - } - - // initialize the regex expressions. - NumbersOnlyRe = regexp.MustCompile(`([^0-9\.\/])`) - SitemapRe = regexp.MustCompile(`sitemap(-([0-9]*)){0,3}.xml`) - - // initialize the main server - s := &http.Server{ - Addr: ":8084", - Handler: http.HandlerFunc(handlerFunc), - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, + log.Fatalln("Error while reading config:", file) } - if err = s.ListenAndServe(); err != nil { - log.Fatalln(err) + config := struct { + BotToken string + ListenAddr string + Resources string + }{ListenAddr: ":8084"} + if err := toml.Unmarshal(file, &config); err != nil { + log.Fatalln("Error while parsing config:", err) } -} - -func handlerFunc(w http.ResponseWriter, r *http.Request) { - // How are we trying to access the site? - switch r.Method { - case http.MethodGet, http.MethodHead: // These methods are allowed. continue. - default: // Send them an error for other ones. - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - return - } - - // Get the pagename. - pagename, values := getPagename(r.URL.EscapedPath()) - - var internal bool - var filename string - var file *os.File - var err error - - // pagename rewrites - switch { - // If the pagename is all numbers then it's a list page. - case !NumbersOnlyRe.Match([]byte(pagename)): - pagename = "list" - // If it's any of the sitemap xml files we want to move to a new function. - case SitemapRe.Match([]byte(pagename)): - XMLServe(w, r, pagename) - return - } - - // Check if it could refer to another internal page - if file, err = os.Open("pages/" + pagename + ".gohtml"); err == nil { - filename = "pages/" + pagename + ".gohtml" - internal = true - // Otherwise, check if it could refer to a regular file. + var fsys fs.FS + if config.Resources != "" { + fsys = os.DirFS(config.Resources) } else { - if file, err = os.Open("./" + pagename); err == nil { - filename = "./" + pagename - } else { - // If all else fails, send a 404. - http.Error(w, err.Error(), 404) - return + if fsys, err = fs.Sub(embedfs, "resources"); err != nil { + log.Fatalln("Error while using embedded resources:") } } - // get the mime-type. - contentType, err := GetContentType(file) + tmpl = template.New("") + tmpl.Funcs(funcMap) + _, err = tmpl.ParseFS(fsys, "templates/*") if err != nil { - http.Error(w, err.Error(), 500) - return + log.Fatalln("Error parsing templates:", err) } - w.Header().Set("Content-Type", contentType) - w.Header().Set("Content-Name", filename) - w.WriteHeader(200) - - var Info struct { - Values []string - Query url.Values + ctx, done := signal.NotifyContext(context.Background(), os.Interrupt) + defer done() + + // TODO: custom store + state := state.New("Bot " + config.BotToken) + state.AddIntents(0 | + gateway.IntentGuildMessages | + gateway.IntentGuilds, + ) + if err = state.Open(ctx); err != nil { + log.Fatalln("Error while opening gateway connection to Discord:", err) } - Info.Values = values - Info.Query = r.URL.Query() - - // Serve the file differently based on whether it's an internal page or not. - if internal { - if err := tmpl.ExecuteTemplate(w, pagename+".gohtml", Info); err != nil { - http.Error(w, err.Error(), 500) - } - } else { - page, err := os.ReadFile(filename) - if err != nil { - http.Error(w, err.Error(), 500) - return - } - w.Write(page) + self, err := state.Me() + if err != nil { + log.Fatalln("Error fetching self:", err) } -} + log.Printf("Connected to Discord as %s#%s (%s)\n", self.Username, self.Discriminator, self.ID) -func getPagename(fullpagename string) (string, []string) { - // Split the pagename into sections - if fullpagename[0] == '/' && len(fullpagename) > 1 { - fullpagename = fullpagename[1:] + server := newServer(state, fsys) + httpserver := &http.Server{ + Addr: config.ListenAddr, + Handler: server, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, } - values_ := strings.Split(fullpagename, "/") - // Filter the values to ones that aren't blank - values := make([]string, 0) - for _, v := range values_ { - if v != "" { - values = append(values, v) + httperr := make(chan error, 1) + go func() { + httperr <- httpserver.ListenAndServe() + }() + select { + case <-ctx.Done(): + done() + err := httpserver.Shutdown(context.Background()) + if err != nil { + log.Fatalln("HTTP server shutdown:", err) } - } - if len(values) == 0 { - values = append(values, "index") - } - - // Then try and get the relevant pagename from that, accounting for many specifics. - pagename := values[0] - switch pagename { - // If it's blank, set it to the default page. - case "": - return "index", values - // If the first part is resources, then treat the rest of the url normally - case "resources": - return fullpagename, values - } - return pagename, values -} - -func GetContentType(output *os.File) (string, error) { - ext := filepath.Ext(output.Name()) - file := make([]byte, 1024) - switch ext { - case ".htm", ".html", ".gohtm", ".gohtml": - return "text/html", nil - case ".css": - return "text/css", nil - case ".js": - return "application/javascript", nil - default: - _, err := output.Read(file) + case err := <-httperr: if err != nil { - return "", err + log.Fatalln("HTTP server encountered error:", err) } - return http.DetectContentType(file), nil } } diff --git a/message.go b/message.go new file mode 100644 index 0000000..f43977d --- /dev/null +++ b/message.go @@ -0,0 +1,163 @@ +package main + +import ( + "html" + "html/template" + "strings" + + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/ningen/v3/discordmd" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/renderer" + mdhtml "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/util" +) + +type Message struct { + discord.Message + RenderedContent template.HTML + MediaPreviews []MediaPreview + PlainAttachments []PlainAttachment +} + +type MediaPreview struct { + Thumbnail template.URL + URL template.URL +} + +type PlainAttachment struct { + Name string + URL template.URL +} + +// message massages a discord.Message into a Message for passing to templates +func (s *server) message(m discord.Message) Message { + msg := Message{ + Message: m, + RenderedContent: s.renderContent(m), + } + var mediapreviews []MediaPreview + for _, e := range m.Embeds { + if e.Thumbnail == nil { + continue + } + var url string + switch { + case e.Video != nil: + if e.Provider != nil { + url = e.URL + } else { + url = e.Video.URL + } + case e.Image != nil: + url = e.Image.Proxy + default: + url = e.Thumbnail.URL + } + mediapreviews = append( + mediapreviews, + MediaPreview{ + template.URL(e.Thumbnail.URL), + template.URL(url), + }, + ) + } + var plainatt []PlainAttachment + for _, att := range m.Attachments { + if att.Height == 0 || + !strings.HasPrefix(att.ContentType, "image/") { + plainatt = append(plainatt, PlainAttachment{ + att.Filename, + template.URL(att.URL), + }) + continue + } + mediapreviews = append(mediapreviews, MediaPreview{ + template.URL(att.URL), template.URL(att.URL), + }) + } + msg.MediaPreviews = mediapreviews + msg.PlainAttachments = plainatt + return msg +} + +func (s *server) renderContent(m discord.Message) template.HTML { + if m.Content != "" && + (len(m.Embeds) == 1 && m.Embeds[0].Type == discord.ImageEmbed && m.Embeds[0].URL == m.Content) { + return "" + } + var sb strings.Builder + src := []byte(m.Content) + ast := discordmd.ParseWithMessage(src, *s.discord.Cabinet, &m, true) + renderer := renderer.NewRenderer( + renderer.WithNodeRenderers( + util.Prioritized(mdhtml.NewRenderer(), 0), + util.Prioritized(mentionRenderer{}, 0), + util.Prioritized(inlineRenderer{}, 0), + ), + ) + renderer.Render(&sb, src, ast) + return template.HTML(sb.String()) +} + +type mentionRenderer struct{} + +func (r mentionRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(discordmd.KindMention, r.render) +} +func (r mentionRenderer) render(writer util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + m := n.(*discordmd.Mention) + switch { + case m.Channel != nil: + writer.WriteString("#") + writer.WriteString(html.EscapeString(m.Channel.Name)) + case m.GuildUser != nil: + writer.WriteString("@") + writer.WriteString(html.EscapeString(m.GuildUser.Username)) + case m.GuildRole != nil: + writer.WriteString("@") + writer.WriteString(html.EscapeString(m.GuildRole.Name)) + } + } + return ast.WalkContinue, nil +} + +type inlineRenderer struct{} + +func (r inlineRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(discordmd.KindInline, r.render) +} + +var attrElements = []struct { + Attr discordmd.Attribute + Element string +}{ + {discordmd.AttrBold, "strong"}, + {discordmd.AttrUnderline, "u"}, + {discordmd.AttrItalics, "em"}, + {discordmd.AttrStrikethrough, "del"}, + {discordmd.AttrMonospace, "code"}, +} + +func (r inlineRenderer) render(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { + i := n.(*discordmd.Inline) + if entering { + for _, at := range attrElements { + if i.Attr.Has(at.Attr) { + w.WriteString("<") + w.WriteString(at.Element) + w.WriteString(">") + } + } + } else { + for _, at := range attrElements { + if i.Attr.Has(at.Attr) { + w.WriteString("") + } + } + } + return ast.WalkContinue, nil +} diff --git a/pages/list.gohtml b/pages/list.gohtml deleted file mode 100644 index 8592852..0000000 --- a/pages/list.gohtml +++ /dev/null @@ -1,10 +0,0 @@ -{{ template "header.gohtml" .}} -{{ $len := len .Values }} -{{ if le $len 1}} - {{template "topics.gohtml" .}} -{{else if le $len 2}} - {{template "messages.gohtml" .}} -{{else}} - {{template "message.gohtml" .}} -{{end}} -{{ template "footer.gohtml" .}} \ No newline at end of file diff --git a/pages/message.gohtml b/pages/message.gohtml deleted file mode 100644 index fc2858b..0000000 --- a/pages/message.gohtml +++ /dev/null @@ -1,56 +0,0 @@ -{{ $guildID := ParseSnowflake (index .Values 0) }} -{{ $channelID := ParseSnowflake (index .Values 1) }} -{{ $postID := ParseSnowflake (index .Values 2) }} - - - {{GetGuildName $guildID}} > {{GetChannelTitle $channelID}} > - {{GetChannelTitle $postID}} - -{{$messages := GetMessagesInChannel $postID}} -{{if $messages.Error}} - error while getting the post; {{$messages.Error}} -{{else}} - {{GetChannelTitle $postID}} - {{$lastAuthor := ""}} - {{$wasDupLastTime := true}} - {{range $_, $v := $messages.Messages}} - {{if or $v.Content $v.Attachments}} - {{if eq $lastAuthor $v.Author.Username}} - {{$wasDupLastTime = true}} - {{else}} - {{if $wasDupLastTime}} - {{template "postfooter" $v}} - {{end}} - - {{template "postheader" $v}} - {{end}} - - {{FormatDiscordThings $guildID .Content}}
- {{range $i, $n := .Attachments}} - - {{end}} - {{end}} - {{$lastAuthor = $v.Author.Username}} - {{end}} -{{end}} - -{{ define "postheader"}} - - - - {{PrettyTime .CreatedAt}} - - - - - {{.Author.Username}}
- -
- -
-
-{{end}} \ No newline at end of file diff --git a/pages/messages.gohtml b/pages/messages.gohtml deleted file mode 100644 index 4697afa..0000000 --- a/pages/messages.gohtml +++ /dev/null @@ -1,29 +0,0 @@ -{{ $guildID := ParseSnowflake (index .Values 0) }} -{{ $channelID := ParseSnowflake (index .Values 1) }} - - - {{GetGuildName $guildID}} > {{GetChannelTitle $channelID}} - - -{{ $threads := GetThreadsInChannel $channelID}} -{{if le (len $threads) 0}} - no posts found; either the bots not in that server or there's no messages in it. -{{else}} - {{GetChannelTitle $channelID}} - - - Posts - Messages - - {{range $i, $v := $threads}} - - - {{$v.Name}} - - - {{len (GetMessagesInChannel $v.ID).Messages}} - - - {{end}} - -{{end}} diff --git a/pages/topics.gohtml b/pages/topics.gohtml deleted file mode 100644 index 35c7b7b..0000000 --- a/pages/topics.gohtml +++ /dev/null @@ -1,27 +0,0 @@ -{{ $guildID := ParseSnowflake (index .Values 0) }} -{{ $forums := GetForums $guildID }} - - {{GetGuildName $guildID}} - - -{{if le (len $forums) 0}} - no forums found; either the bots not in that server or there's no forum channels in it. -{{else}} - {{GetGuildName $guildID}} - - - Forum - Posts - - {{range $i, $v := $forums}} - - - {{$v.Name}} - - - {{len (GetThreadsInChannel $v.ID)}} - - - {{end}} - -{{end}} diff --git a/resources/style.css b/resources/static/style.css similarity index 96% rename from resources/style.css rename to resources/static/style.css index dcb039d..1f8881d 100644 --- a/resources/style.css +++ b/resources/static/style.css @@ -27,6 +27,10 @@ p + br + p { display: block; } +.post-info { + white-space: pre-line; +} + .title, .threadnum { padding: 5px; } @@ -92,4 +96,4 @@ p + br + p { padding: 15px; margin: 10px 0 50px 0; background: #ddd; -} \ No newline at end of file +} diff --git a/resources/templates/error.gohtml b/resources/templates/error.gohtml new file mode 100644 index 0000000..a0fd8fa --- /dev/null +++ b/resources/templates/error.gohtml @@ -0,0 +1,6 @@ +{{ template "header.gohtml" }} +

{{.StatusCode}} {{.StatusText}}

+{{with .Error}} +

{{.}}

+{{end}} +{{template "footer.gohtml" }} diff --git a/pages/footer.gohtml b/resources/templates/footer.gohtml similarity index 100% rename from pages/footer.gohtml rename to resources/templates/footer.gohtml diff --git a/resources/templates/forum.gohtml b/resources/templates/forum.gohtml new file mode 100644 index 0000000..aa29d57 --- /dev/null +++ b/resources/templates/forum.gohtml @@ -0,0 +1,23 @@ +{{ template "header.gohtml" .}} + + {{.Guild.Name}} > {{.Forum.Name}} + + +{{.Forum.Name}} + + + Posts + Messages + + {{range .Posts}} + + + {{.Name}} + + + {{.MessageCount}} + + + {{end}} + +{{ template "footer.gohtml" .}} diff --git a/resources/templates/guild.gohtml b/resources/templates/guild.gohtml new file mode 100644 index 0000000..c4debb7 --- /dev/null +++ b/resources/templates/guild.gohtml @@ -0,0 +1,22 @@ +{{ template "header.gohtml" .}} + + {{.Guild.Name}} + + {{.Guild.Name}} + + + Forum + Posts + + {{range $_, $forum := .ForumChannels}} + + + {{$forum.Name}} + + + {{len $forum.Posts}} + + + {{end}} + +{{ template "footer.gohtml" .}} diff --git a/pages/header.gohtml b/resources/templates/header.gohtml similarity index 84% rename from pages/header.gohtml rename to resources/templates/header.gohtml index 332e8ab..e38e498 100644 --- a/pages/header.gohtml +++ b/resources/templates/header.gohtml @@ -3,11 +3,11 @@ - +

dfs

-
\ No newline at end of file + diff --git a/pages/index.gohtml b/resources/templates/index.gohtml similarity index 92% rename from pages/index.gohtml rename to resources/templates/index.gohtml index 716d4c5..c1f7e39 100644 --- a/pages/index.gohtml +++ b/resources/templates/index.gohtml @@ -12,5 +12,5 @@

once the bot is invited, you can go to dfs.ioi-xd.net/(THE ID OF YOUR GUILD) to see the messages within it. -

currently serving {{GuildNum}} servers.

-{{template "footer.gohtml" }} \ No newline at end of file +

currently serving {{.GuildCount}} servers.

+{{template "footer.gohtml" }} diff --git a/resources/templates/post.gohtml b/resources/templates/post.gohtml new file mode 100644 index 0000000..9a57d4f --- /dev/null +++ b/resources/templates/post.gohtml @@ -0,0 +1,40 @@ +{{ template "header.gohtml" .}} + + {{.Guild.Name}} > {{.Forum.Name}} > + {{.Post.Name}} + +{{range .MessageGroups}} + + + + {{PrettyTime (index . 0).ID.Time}} + + + + + {{(index . 0).Author.Username}}
+ +
+ +
+
+{{end}} +{{ template "footer.gohtml" .}} diff --git a/server.go b/server.go new file mode 100644 index 0000000..76cc008 --- /dev/null +++ b/server.go @@ -0,0 +1,279 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "sort" + "sync" + "time" + + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/state" + "github.com/diamondburned/arikawa/v3/utils/httputil" + "github.com/go-chi/chi/v5" +) + +type server struct { + r *chi.Mux + + discord *state.State + + sitemap []byte + sitemapUpdated time.Time + sitemapMu sync.Mutex + + buffers *sync.Pool +} + +func newServer(discord *state.State, fsys fs.FS) *server { + s := new(server) + s.discord = discord + s.buffers = &sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, + } + r := chi.NewRouter() + s.r = r + r.Get(`/sitemap.xml`, s.getSitemap) + r.Get("/", s.getIndex) + r.Route("/{guildID:\\d+}", func(r chi.Router) { + r.Get("/", s.getGuild) + r.Route("/{forumID:\\d+}", func(r chi.Router) { + r.Get("/", s.getForum) + r.Route("/{postID:\\d+}", func(r chi.Router) { + r.Get("/", s.getPost) + }) + }) + }) + r.Get("/static/*", http.FileServer(http.FS(fsys)).ServeHTTP) + r.NotFound(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + displayErr(w, http.StatusNotFound, nil) + })) + return s +} + +func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.r.ServeHTTP(w, r) +} + +func (s *server) executeTemplate(w http.ResponseWriter, name string, ctx any) { + buf := s.buffers.Get().(*bytes.Buffer) + if err := tmpl.ExecuteTemplate(buf, name, ctx); err == nil { + io.Copy(w, buf) + } else { + displayErr(w, http.StatusInternalServerError, err) + } + buf.Reset() + s.buffers.Put(buf) +} + +func displayErr(w http.ResponseWriter, status int, err error) { + ctx := struct { + Error error + StatusText string + StatusCode int + }{err, http.StatusText(status), status} + w.WriteHeader(status) + tmpl.ExecuteTemplate(w, "error.gohtml", ctx) +} + +func discordStatusIs(err error, status int) bool { + var httperr *httputil.HTTPError + if ok := errors.As(err, &httperr); !ok { + return false + } + return httperr.Status == status +} + +func (s *server) getIndex(w http.ResponseWriter, r *http.Request) { + guilds, err := s.discord.Cabinet.Guilds() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + ctx := struct { + GuildCount int + }{len(guilds)} + s.executeTemplate(w, "index.gohtml", ctx) +} + +type ForumChannel struct { + discord.Channel + Posts []discord.Channel +} + +func (s *server) getGuild(w http.ResponseWriter, r *http.Request) { + guild, ok := s.guildFromReq(w, r) + if !ok { + return + } + ctx := struct { + Guild *discord.Guild + ForumChannels []ForumChannel + }{Guild: guild} + channels, err := s.discord.Channels(guild.ID) + if err != nil { + displayErr(w, http.StatusInternalServerError, + fmt.Errorf("fetching guild channels: %s", err)) + return + } + threads, err := s.discord.ActiveThreads(guild.ID) + if err != nil { + displayErr(w, http.StatusInternalServerError, + fmt.Errorf("fetching guild active threads: %s", err)) + return + } + for _, ch := range channels { + if ch.Type == discord.GuildForum { + var posts []discord.Channel + for _, t := range threads.Threads { + if t.ParentID == ch.ID { + posts = append(posts, t) + } + } + ctx.ForumChannels = append(ctx.ForumChannels, ForumChannel{ + ch, posts, + }) + } + } + s.executeTemplate(w, "guild.gohtml", ctx) +} + +func (s *server) getForum(w http.ResponseWriter, r *http.Request) { + guild, ok := s.guildFromReq(w, r) + if !ok { + return + } + forum, ok := s.forumFromReq(w, r) + if !ok { + return + } + ctx := struct { + Guild *discord.Guild + Forum *discord.Channel + Posts []discord.Channel + }{guild, forum, nil} + guildThreads, err := s.discord.ActiveThreads(guild.ID) + if err != nil { + displayErr(w, http.StatusInternalServerError, + fmt.Errorf("fetching guild threads: %w", err)) + } + for _, t := range guildThreads.Threads { + if t.ParentID == forum.ID { + ctx.Posts = append(ctx.Posts, t) + } + } + s.executeTemplate(w, "forum.gohtml", ctx) +} + +func (s *server) getPost(w http.ResponseWriter, r *http.Request) { + guild, ok := s.guildFromReq(w, r) + if !ok { + return + } + forum, ok := s.forumFromReq(w, r) + if !ok { + return + } + post, ok := s.postFromReq(w, r) + if !ok { + return + } + ctx := struct { + Guild *discord.Guild + Forum *discord.Channel + Post *discord.Channel + MessageGroups [][]Message + }{guild, forum, post, nil} + msgs, err := s.discord.Client.Messages(post.ID, 0) + if err != nil { + displayErr(w, http.StatusInternalServerError, + fmt.Errorf("fetching post's messages: %w", err)) + return + } + sort.Slice(msgs, func(i, j int) bool { + return msgs[i].ID < msgs[j].ID + }) + var msgrps [][]Message + i := 0 + for _, m := range msgs { + if len(msgrps) == i { + grp := []Message{ + s.message(m), + } + msgrps = append(msgrps, grp) + } else { + if msgrps[i][0].Author == m.Author { + msgrps[i] = append(msgrps[i], s.message(m)) + } else { + i++ + } + } + } + ctx.MessageGroups = msgrps + s.executeTemplate(w, "post.gohtml", ctx) +} + +func (s *server) guildFromReq(w http.ResponseWriter, r *http.Request) (*discord.Guild, bool) { + guildIDsf, err := discord.ParseSnowflake(chi.URLParam(r, "guildID")) + if err != nil { + displayErr(w, http.StatusBadRequest, err) + return nil, false + } + guildID := discord.GuildID(guildIDsf) + guild, err := s.discord.Guild(guildID) + if err != nil { + if discordStatusIs(err, http.StatusNotFound) { + displayErr(w, http.StatusNotFound, nil) + } else { + displayErr(w, http.StatusInternalServerError, + fmt.Errorf("fetching guild: %w", err)) + } + return nil, false + } + return guild, true +} + +func (s *server) forumFromReq(w http.ResponseWriter, r *http.Request) (*discord.Channel, bool) { + forumIDsf, err := discord.ParseSnowflake(chi.URLParam(r, "forumID")) + if err != nil { + displayErr(w, http.StatusBadRequest, err) + return nil, false + } + forumID := discord.ChannelID(forumIDsf) + forum, err := s.discord.Channel(forumID) + if err != nil { + if discordStatusIs(err, http.StatusNotFound) { + displayErr(w, http.StatusNotFound, nil) + } else { + displayErr(w, http.StatusInternalServerError, + fmt.Errorf("fetching forum: %w", err)) + } + return nil, false + } + return forum, true +} + +func (s *server) postFromReq(w http.ResponseWriter, r *http.Request) (*discord.Channel, bool) { + postIDsf, err := discord.ParseSnowflake(chi.URLParam(r, "postID")) + if err != nil { + displayErr(w, http.StatusBadRequest, err) + return nil, false + } + postID := discord.ChannelID(postIDsf) + post, err := s.discord.Channel(postID) + if err != nil { + if discordStatusIs(err, http.StatusNotFound) { + displayErr(w, http.StatusNotFound, nil) + } else { + displayErr(w, http.StatusInternalServerError, + fmt.Errorf("fetching post: %w", err)) + } + return nil, false + } + return post, true +} diff --git a/sitemap.go b/sitemap.go index 39d731f..c6f583f 100644 --- a/sitemap.go +++ b/sitemap.go @@ -2,189 +2,121 @@ package main import ( "bytes" - "compress/gzip" "encoding/xml" "fmt" + "io" "net/http" - "strconv" - "strings" "time" - "github.com/disgoorg/snowflake/v2" + "github.com/diamondburned/arikawa/v3/discord" ) -const ( - XMLIndexSettings = `xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"` - XMLListSettings = `xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"` - XMLPageHeader = `` - XMLURLPageHeader = `` - XMLSitemapPageHeader = `` - XMLSitemapPageFooter = `` - XMLURLPageFooter = `` -) - -var dontCare = strings.NewReplacer( - "sitemap-", "", - "sitemap", "", - ".xml", "", -) - -type CacheObject struct { - XMLPage []byte - LastUpdated int64 - GZipped bool -} - -var Caches map[string]CacheObject - -type Sitemap struct { - Location string `xml:"loc"` -} -type SitemapIndex []Sitemap type URL struct { + XMLName string `xml:"url"` Location string `xml:"loc"` - LastMod string `xml:"lastmod"` - Frequency string `xml:"changefreq"` - Priority float32 `xml:"priority"` -} -type URLSet []URL - -var XMLPage string // The xml page to serve. -var LastUpdatedFormat string // obsolete but i'm too tired to remove it - -func init() { - Caches = make(map[string]CacheObject) + LastMod string `xml:"lastmod,omitempty"` + Frequency string `xml:"changefreq,omitempty"` + Priority float32 `xml:"priority,omitempty"` } -func XMLServe(w http.ResponseWriter, r *http.Request, pagename string) { - var XMLPage []byte - var gz bool - obj, ok := Caches[pagename] - // If it's not existent, cache it. - if !ok { - XMLPage, gz = XMLPageGen(pagename) - - newOBJ := CacheObject{ - XMLPage: XMLPage, - GZipped: gz, - LastUpdated: time.Now().Unix(), - } - Caches[pagename] = newOBJ - } else { - // Otherwise, search the cache. - lastUpdated := obj.LastUpdated - // If the time 30 minutes ago is greater then the updated time - if (time.Now().Unix() - int64(time.Hour*6)) > lastUpdated { - // Update the cache - obj.XMLPage, obj.GZipped = XMLPageGen(pagename) - obj.LastUpdated = time.Now().Unix() +func (s *server) getSitemap(w http.ResponseWriter, r *http.Request) { + s.sitemapMu.Lock() + var sitemap []byte + var modtime time.Time + if s.sitemap == nil || time.Since(s.sitemapUpdated) > 6*time.Hour { + buf := s.buffers.Get().(*bytes.Buffer) + err := s.writeSitemap(buf) + if err != nil { + s.sitemapMu.Unlock() + buf.Reset() + s.buffers.Put(buf) + http.Error(w, err.Error(), http.StatusInternalServerError) + return } - // Otherwise, serve that cached version. - XMLPage, gz = obj.XMLPage, obj.GZipped - } - w.Header().Set("Content-Name", pagename) - if gz { - w.Header().Set("Content-Type", "application/gzip") - w.Header().Set("Content-Disposition", "attachment; filename="+pagename) + sitemap := make([]byte, buf.Len()) + copy(sitemap, buf.Bytes()) + s.sitemap = sitemap + s.sitemapUpdated = time.Now() + modtime = s.sitemapUpdated + s.sitemapMu.Unlock() + buf.Reset() + s.buffers.Put(buf) } else { - w.Header().Set("Content-Type", "text/xml") - w.Header().Set("Content-Name", pagename) + sitemap = s.sitemap + modtime = s.sitemapUpdated + s.sitemapMu.Unlock() } - - w.Write(XMLPage) + rdr := bytes.NewReader(sitemap) + http.ServeContent(w, r, "sitemap.xml", modtime, rdr) } -// todo: scrap this function in favor of go-chi's pagnation system. -func XMLPageGen(pagename string) (XMLPage []byte, gz bool) { - // get the values and stuff we want from the pagename - pagename = dontCare.Replace(pagename) - - // whether or not to serve the gzipped file - if strings.Contains(pagename, ".gz") { - pagename = strings.Replace(pagename, ".gz", "", 1) - gz = true - } else { - gz = false - } - - var XMLResult string - parts := strings.Split(pagename, "-") - - if parts[0] == "" { - XMLResult = XMLPageGenGuilds() - } else { - var guildID int - guildID, err := strconv.Atoi(parts[0]) - if err != nil { - return []byte(err.Error()), false - } - XMLResult = XMLPageGenThreads(snowflake.ID(guildID)) - } - - if gz { - return GZIPString(XMLResult), true - } else { - return []byte(XMLResult), false - } +var XMLURLSetStart = xml.StartElement{ + Name: xml.Name{Local: "urlset"}, + Attr: []xml.Attr{ + {xml.Name{Local: "xmlns"}, "http://www.sitemaps.org/schemas/sitemap/0.9"}, + }, } -func XMLPageGenGuilds() string { - var XMLPage bytes.Buffer - var sitemapIndex SitemapIndex +var XMLURLSetEnd = XMLURLSetStart.End() - guilds := Client.Client.Caches().Guilds().All() - for _, g := range guilds { - sitemapIndex = append(sitemapIndex, Sitemap{ - Location: fmt.Sprintf("https://dfs.ioi-xd.net/sitemap-%v.xml.gz", g.ID), - }) +func (s *server) writeSitemap(w io.Writer) error { + if _, err := io.WriteString(w, xml.Header); err != nil { + return err } - output, err := xml.Marshal(sitemapIndex) - if err != nil { - return err.Error() + enc := xml.NewEncoder(w) + var err error + if err = enc.EncodeToken(XMLURLSetStart); err != nil { + return err } - XMLPage.Write([]byte(XMLSitemapPageHeader)) - XMLPage.Write(output) - XMLPage.Write([]byte(XMLSitemapPageFooter)) - return XMLPage.String() -} - -func XMLPageGenThreads(guildID snowflake.ID) string { - var XMLPage bytes.Buffer - var urlIndex URLSet + guilds, _ := s.discord.Cabinet.Guilds() + for _, guild := range guilds { + if err = enc.Encode(URL{ + Location: fmt.Sprintf("https://dfs.ioi-xd.net/%s", guild.ID), + }); err != nil { + return err + } - channels := Client.GetForums(guildID) - for _, c := range channels { - threads := Client.GetThreadsInChannel(c.ID()) - // todo: have this actually reflect when the channel was last updated. - lastUpdatedFormat := time.Now().Format(time.RFC3339) - for _, t := range threads { - urlIndex = append(urlIndex, URL{ - Location: fmt.Sprintf("https://dfs.ioi-xd.net/%v/%v/%v", guildID, c.ID(), t.ID()), - LastMod: lastUpdatedFormat, - Frequency: "hourly", - Priority: 1.0, - }) + channels, err := s.discord.Channels(guild.ID) + if err != nil { + return err + } + threads, err := s.discord.ActiveThreads(guild.ID) + if err != nil { + return err + } + for _, channel := range channels { + if channel.Type != discord.GuildForum { + continue + } + if err = enc.Encode(URL{ + Location: fmt.Sprintf("https://dfs.ioi-xd.net/%s/%s", guild.ID, channel.ID), + }); err != nil { + return err + } + } + for _, thread := range threads.Threads { + for _, channel := range channels { + if channel.Type != discord.GuildForum { + continue + } + if thread.ParentID != channel.ID { + continue + } + if err = enc.Encode(URL{ + Location: fmt.Sprintf("https://dfs.ioi-xd.net/%s/%s/%s", guild.ID, channel.ID, thread.ID), + }); err != nil { + return err + } + break + } } } - output, err := xml.Marshal(urlIndex) - if err != nil { - return err.Error() + if err = enc.EncodeToken(XMLURLSetEnd); err != nil { + return err } - XMLPage.Write([]byte(XMLURLPageHeader)) - XMLPage.Write(output) - XMLPage.Write([]byte(XMLURLPageFooter)) - return XMLPage.String() -} - -func GZIPString(page string) (result []byte) { - var b bytes.Buffer - gzipWriter := gzip.NewWriter(&b) - _, err := gzipWriter.Write([]byte(page)) - // todo: better error handling. - if err != nil { - fmt.Println(err) - return + if err = enc.Flush(); err != nil { + return err } - gzipWriter.Close() - return b.Bytes() + _, err = w.Write([]byte{'\n'}) + return err } diff --git a/strings.go b/strings.go deleted file mode 100644 index 0709438..0000000 --- a/strings.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" - - "github.com/gomarkdown/markdown" -) - -// Capitalize a string -func Capitalize(value string) string { - // Treat dashes as spaces - value = strings.Replace(value, "-", " ", 99) - valuesplit := strings.Split(value, " ") - var result string - for _, v := range valuesplit { - if len(v) <= 0 { - continue - } - result += strings.ToUpper(v[:1]) - result += v[1:] + " " - } - return result -} - -// Trim a string to 128 characters, for meta tags. -func TrimForMeta(value string) string { - if len(value) <= 127 { - return value - } - return value[:128] + "..." -} - -// Parsing a markdown string. - -func Markdown(val string) []byte { - return markdown.ToHTML([]byte(val), nil, nil) -} - -// Function for formatting a timestamp as "x hours ago" -func PrettyTime(timestamp time.Time) string { - unixTimeDur := time.Now().Sub(timestamp) - - if unixTimeDur.Hours() >= 8760 { - return fmt.Sprintf("%0.f years ago", unixTimeDur.Hours()/8760) - } - if unixTimeDur.Hours() >= 730 { - return fmt.Sprintf("%0.f months ago", unixTimeDur.Hours()/730) - } - if unixTimeDur.Hours() >= 168 { - return fmt.Sprintf("%0.f weeks ago", unixTimeDur.Hours()/168) - } - if unixTimeDur.Hours() >= 24 { - return fmt.Sprintf("%0.f days ago", unixTimeDur.Hours()/24) - } - if unixTimeDur.Hours() >= 1 { - return fmt.Sprintf("%0.f hours ago", unixTimeDur.Hours()) - } - if unixTimeDur.Minutes() >= 1 { - return fmt.Sprintf("%0.f minutes ago", unixTimeDur.Minutes()) - } - return fmt.Sprintf("%0.f seconds ago", unixTimeDur.Seconds()) -}