├── .dockerignore ├── .gitattributes ├── .gitignore ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── README.md ├── bot ├── bot.go ├── commandler │ ├── command.go │ ├── commandler.go │ ├── context.go │ └── events.go ├── commands.go ├── events.go ├── localization │ └── localization.go ├── settings │ ├── settings.go │ └── types.go ├── starboard.go ├── tables │ └── tables.go ├── util │ ├── constants.go │ └── util.go └── utils.go ├── docker-compose.yml ├── env └── prod.example.env ├── locales ├── README.md ├── de-DE.json ├── en-US.json └── nl-NL.json └── main.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .gitattributes 2 | .gitignore 3 | env 4 | vendor 5 | README.md -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env/* 2 | !env/prod.example.env 3 | vendor -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine 2 | 3 | WORKDIR /go/src 4 | COPY . github.com/xdimgg/starboard 5 | WORKDIR /go/src/github.com/xdimgg/starboard 6 | 7 | RUN apk update 8 | RUN apk upgrade 9 | RUN apk add git curl --no-cache 10 | RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 11 | RUN dep ensure 12 | RUN go build -o /bin/starboard 13 | RUN apk del golang* curl 14 | 15 | ENTRYPOINT /bin/starboard -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "develop" 6 | digest = "1:9046a503999f28d21b76210f7069ada1f4dc2be92f634db722826b92496d90fb" 7 | name = "github.com/bwmarrin/discordgo" 8 | packages = ["."] 9 | pruneopts = "UT" 10 | revision = "11ffb200c7539bacd23c92ab0dc3b8ec128040aa" 11 | source = "github.com/xdimgg/discordgo" 12 | 13 | [[projects]] 14 | digest = "1:fed1f537c2f1269fe475a8556c393fe466641682d73ef8fd0491cd3aa1e47bad" 15 | name = "github.com/certifi/gocertifi" 16 | packages = ["."] 17 | pruneopts = "UT" 18 | revision = "deb3ae2ef2610fde3330947281941c562861188b" 19 | version = "2018.01.18" 20 | 21 | [[projects]] 22 | branch = "master" 23 | digest = "1:608d0fe1621755354728fbaeef8ada8b4a33001aec88b66c970e641be950d850" 24 | name = "github.com/getsentry/raven-go" 25 | packages = ["."] 26 | pruneopts = "UT" 27 | revision = "7535a8fa2ace0ffb684b19f28d00655459ea2dae" 28 | 29 | [[projects]] 30 | digest = "1:5b14f08247db438a2d11b77aff373b6536028673e38eab87fb5062e07b3b7487" 31 | name = "github.com/go-pg/pg" 32 | packages = [ 33 | ".", 34 | "internal", 35 | "internal/parser", 36 | "internal/pool", 37 | "orm", 38 | "types", 39 | ] 40 | pruneopts = "UT" 41 | revision = "c0368cd6ee62269f0a5c3d6c9c02c6d192760bc6" 42 | version = "v6.14.3" 43 | 44 | [[projects]] 45 | digest = "1:991bb96360eb8db70a40e9c496769fe6a7bbac4047f8376494d9837397078327" 46 | name = "github.com/go-redis/redis" 47 | packages = [ 48 | ".", 49 | "internal", 50 | "internal/consistenthash", 51 | "internal/hashtag", 52 | "internal/pool", 53 | "internal/proto", 54 | "internal/singleflight", 55 | "internal/util", 56 | ] 57 | pruneopts = "UT" 58 | revision = "480db94d33e6088e08d628833b6c0705451d24bb" 59 | version = "v6.13.2" 60 | 61 | [[projects]] 62 | digest = "1:cee8e8ac80df6373e7daa11baf1f98c1b6f7242c49ccae7e1ec34a971dc408d9" 63 | name = "github.com/gorilla/websocket" 64 | packages = ["."] 65 | pruneopts = "UT" 66 | revision = "3ff3320c2a1756a3691521efc290b4701575147c" 67 | version = "v1.3.0" 68 | 69 | [[projects]] 70 | branch = "master" 71 | digest = "1:fd97437fbb6b7dce04132cf06775bd258cce305c44add58eb55ca86c6c325160" 72 | name = "github.com/jinzhu/inflection" 73 | packages = ["."] 74 | pruneopts = "UT" 75 | revision = "04140366298a54a039076d798123ffa108fff46c" 76 | 77 | [[projects]] 78 | branch = "master" 79 | digest = "1:eecf61f92de3371592dc7b74ad421cdade60189c807f487a989b45f9fd35febe" 80 | name = "github.com/jonas747/dshardmanager" 81 | packages = ["."] 82 | pruneopts = "UT" 83 | revision = "fcc4559ea1f3ab926126de92f6fdf58945b9c4c5" 84 | 85 | [[projects]] 86 | digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747" 87 | name = "github.com/pkg/errors" 88 | packages = ["."] 89 | pruneopts = "UT" 90 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d" 91 | version = "v0.8.0" 92 | 93 | [[projects]] 94 | branch = "master" 95 | digest = "1:a68068595227881a09dd5961976ddf705c8eea822dcd1c7b57bbb0067335ad32" 96 | name = "golang.org/x/crypto" 97 | packages = [ 98 | "internal/subtle", 99 | "nacl/secretbox", 100 | "poly1305", 101 | "salsa20/salsa", 102 | ] 103 | pruneopts = "UT" 104 | revision = "614d502a4dac94afa3a6ce146bd1736da82514c6" 105 | 106 | [solve-meta] 107 | analyzer-name = "dep" 108 | analyzer-version = 1 109 | input-imports = [ 110 | "github.com/bwmarrin/discordgo", 111 | "github.com/getsentry/raven-go", 112 | "github.com/go-pg/pg", 113 | "github.com/go-pg/pg/orm", 114 | "github.com/go-redis/redis", 115 | "github.com/jinzhu/inflection", 116 | "github.com/jonas747/dshardmanager", 117 | ] 118 | solver-name = "gps-cdcl" 119 | solver-version = 1 120 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [prune] 29 | go-tests = true 30 | unused-packages = true 31 | 32 | [[constraint]] 33 | name = "github.com/bwmarrin/discordgo" 34 | branch = "develop" 35 | source = "github.com/xdimgg/discordgo" 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starboard 2 | 3 | Starboard is a bot simply dedicated to having a Starboard in your Discord server. -------------------------------------------------------------------------------- /bot/bot.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "runtime/debug" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/GitbookIO/syncgroup" 15 | 16 | "github.com/xdimgg/starboard/bot/util" 17 | 18 | "github.com/bwmarrin/discordgo" 19 | raven "github.com/getsentry/raven-go" 20 | "github.com/go-pg/pg" 21 | "github.com/go-pg/pg/orm" 22 | "github.com/go-redis/redis" 23 | "github.com/jonas747/dshardmanager" 24 | "github.com/xdimgg/starboard/bot/commandler" 25 | "github.com/xdimgg/starboard/bot/localization" 26 | "github.com/xdimgg/starboard/bot/settings" 27 | "github.com/xdimgg/starboard/bot/tables" 28 | ) 29 | 30 | const ( 31 | settingPrefix = "prefix" 32 | settingLanguage = "language" 33 | settingMinimum = "minimum" 34 | settingSelfStar = "self_star" 35 | settingSelfStarWarning = "self_star_warning" 36 | settingEmoji = "emoji" 37 | settingChannel = "channel" 38 | settingNSFWChannel = "nsfw_channel" 39 | settingMinimal = "minimal" 40 | settingRemoveBotStars = "remove_bot_stars" 41 | settingSaveDeletedMessages = "save_deleted_messages" 42 | settingBlockMode = "block_mode" 43 | settingRandomStarProbability = "random_star_probability" 44 | 45 | settingNone = "none" 46 | ) 47 | 48 | const starEmoji = "⭐" 49 | 50 | // Bot represents a starboard instance 51 | type Bot struct { 52 | PG *pg.DB 53 | Redis *redis.Client 54 | Sentry *raven.Client 55 | Manager *dshardmanager.Manager 56 | Locales *localization.Locales 57 | Settings *settings.Settings 58 | StartTime time.Time 59 | 60 | expectedGuilds map[*discordgo.Session]int 61 | mutexGroup *syncgroup.MutexGroup 62 | opts *Options 63 | } 64 | 65 | // DiscordList represents a Discord Bot list 66 | type DiscordList struct { 67 | Authorization string 68 | URL func(id string) string 69 | Serialize func(shardCount, guildCount int) ([]byte, error) 70 | } 71 | 72 | // Options represents the options for creating a starboard instance 73 | type Options struct { 74 | Prefix string 75 | Token string 76 | Locales string 77 | OwnerID string 78 | Mode string 79 | SentryDSN string 80 | DiscordLists []DiscordList 81 | 82 | Guild string 83 | GuildLogChannel string 84 | MemberLogChannel string 85 | } 86 | 87 | // New creates a starboard instance 88 | func New(opts *Options, pgOpts *pg.Options, redisOpts *redis.Options) (err error) { 89 | b := &Bot{ 90 | PG: pg.Connect(pgOpts), 91 | Redis: redis.NewClient(redisOpts), 92 | StartTime: time.Now(), 93 | 94 | expectedGuilds: make(map[*discordgo.Session]int), 95 | mutexGroup: syncgroup.NewMutexGroup(), 96 | opts: opts, 97 | } 98 | 99 | b.Sentry, err = raven.New(opts.SentryDSN) 100 | if err != nil { 101 | return 102 | } 103 | 104 | b.Locales, err = localization.New(b.opts.Locales) 105 | if err != nil { 106 | return 107 | } 108 | 109 | b.Settings, err = settings.New(b.PG, map[string]interface{}{ 110 | settingPrefix: b.opts.Prefix, 111 | settingLanguage: "en-US", 112 | settingMinimum: 1, 113 | settingSelfStar: false, 114 | settingSelfStarWarning: false, 115 | settingEmoji: &util.Emoji{ 116 | Name: "star", 117 | Unicode: starEmoji, 118 | }, 119 | settingChannel: settingNone, 120 | settingNSFWChannel: settingNone, 121 | settingMinimal: false, 122 | settingRemoveBotStars: true, 123 | settingSaveDeletedMessages: false, 124 | settingBlockMode: "blacklist", 125 | settingRandomStarProbability: float64(0), 126 | }) 127 | if err != nil { 128 | return 129 | } 130 | 131 | err = b.createTables((*tables.Message)(nil), (*tables.Reaction)(nil), (*tables.Block)(nil)) 132 | if err != nil { 133 | return 134 | } 135 | 136 | b.Manager = dshardmanager.New("Bot " + b.opts.Token) 137 | b.Manager.SessionFunc = dshardmanager.SessionFunc(func(token string) (s *discordgo.Session, err error) { 138 | s, err = discordgo.New(token) 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | s.State.TrackEmojis = false 144 | s.State.TrackPresences = false 145 | s.State.TrackVoice = false 146 | 147 | s.AddHandler(b.ready) 148 | 149 | if b.opts.GuildLogChannel != "" { 150 | s.AddHandler(b.guildCreate) 151 | s.AddHandler(b.guildDelete) 152 | } 153 | 154 | if b.opts.Guild != "" && b.opts.MemberLogChannel != "" { 155 | s.AddHandler(b.guildMemberAdd) 156 | s.AddHandler(b.guildMemberRemove) 157 | } 158 | 159 | s.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { 160 | tags := map[string]string{"event": "MESSAGE_CREATE"} 161 | b.Sentry.CapturePanic(func() { 162 | b.reportError(b.messageCreate(s, m), tags) 163 | }, tags) 164 | }) 165 | 166 | s.AddHandler(func(s *discordgo.Session, m *discordgo.MessageUpdate) { 167 | tags := map[string]string{"event": "MESSAGE_UPDATE"} 168 | b.Sentry.CapturePanic(func() { 169 | b.reportError(b.messageUpdate(s, m), tags) 170 | }, tags) 171 | }) 172 | 173 | s.AddHandler(func(s *discordgo.Session, m *discordgo.MessageDelete) { 174 | tags := map[string]string{"event": "MESSAGE_DELETE"} 175 | b.Sentry.CapturePanic(func() { 176 | b.reportError(b.messageDelete(s, m), tags) 177 | }, tags) 178 | }) 179 | 180 | s.AddHandler(func(s *discordgo.Session, m *discordgo.MessageDeleteBulk) { 181 | tags := map[string]string{"event": "MESSAGE_DELETE_BULK"} 182 | b.Sentry.CapturePanic(func() { 183 | b.reportError(b.messageDeleteBulk(s, m), tags) 184 | }, tags) 185 | }) 186 | 187 | s.AddHandler(func(s *discordgo.Session, m *discordgo.MessageReactionAdd) { 188 | tags := map[string]string{"event": "MESSAGE_REACTION_ADD"} 189 | b.Sentry.CapturePanic(func() { 190 | b.reportError(b.messageReactionAdd(s, m), tags) 191 | }, tags) 192 | }) 193 | 194 | s.AddHandler(func(s *discordgo.Session, m *discordgo.MessageReactionRemove) { 195 | tags := map[string]string{"event": "MESSAGE_REACTION_REMOVE"} 196 | b.Sentry.CapturePanic(func() { 197 | b.reportError(b.messageReactionRemove(s, m), tags) 198 | }, tags) 199 | }) 200 | 201 | s.AddHandler(func(s *discordgo.Session, m *discordgo.MessageReactionRemoveAll) { 202 | tags := map[string]string{"event": "MESSAGE_REACTION_REMOVE_ALL"} 203 | b.Sentry.CapturePanic(func() { 204 | b.reportError(b.messageReactionRemoveAll(s, m), tags) 205 | }, tags) 206 | }) 207 | 208 | c := commandler.New(s, b.Locales, b.Settings) 209 | c.OwnerID = b.opts.OwnerID 210 | b.registerCommands(c) 211 | 212 | if b.prod() { 213 | c.SetOnError(func(ctx *commandler.Context, err error, panicked bool) { 214 | b.reportError(err, map[string]string{ 215 | "command": ctx.Command.Name, 216 | "args": strings.Join(ctx.Args, " "), 217 | "panicked": strconv.FormatBool(panicked), 218 | }) 219 | 220 | ctx.Say("error") 221 | }) 222 | } else { 223 | c.SetOnError(func(ctx *commandler.Context, err error, panicked bool) { 224 | _, mErr := ctx.SayRaw(fmt.Sprintf("Nice error, dumbass\nPanicked: `%t`\nError:\n```\n%s\n```\nStack trace:\n```\n%s\n```", panicked, err.Error(), debug.Stack()[:1500])) 225 | if mErr != nil { 226 | fmt.Println(mErr) 227 | } 228 | }) 229 | } 230 | 231 | return 232 | }) 233 | 234 | err = b.Manager.Start() 235 | if err != nil { 236 | return 237 | } 238 | 239 | b.initStatPoster(time.Minute) 240 | return 241 | } 242 | 243 | func (b *Bot) initStatPoster(d time.Duration) { 244 | var shardCount, guildCount int 245 | 246 | for range time.NewTicker(d).C { 247 | newShardCount := len(b.Manager.Sessions) 248 | newGuildCount := 0 249 | 250 | for _, s := range b.Manager.Sessions { 251 | newGuildCount += len(s.State.Guilds) 252 | } 253 | 254 | if shardCount == newShardCount && guildCount == newGuildCount { 255 | continue 256 | } 257 | 258 | shardCount = newShardCount 259 | guildCount = newGuildCount 260 | 261 | for _, list := range b.opts.DiscordLists { 262 | if list.Authorization == "" { 263 | continue 264 | } 265 | 266 | url := list.URL(b.Manager.Sessions[0].State.User.ID) 267 | 268 | data, err := list.Serialize(shardCount, guildCount) 269 | if err != nil { 270 | b.reportError(err, map[string]string{"url": url}) 271 | continue 272 | } 273 | 274 | req, err := http.NewRequest("POST", url, bytes.NewReader(data)) 275 | if err != nil { 276 | b.reportError(err, map[string]string{"url": url}) 277 | continue 278 | } 279 | 280 | req.Header.Set("Authorization", list.Authorization) 281 | req.Header.Set("Content-Type", "application/json") 282 | 283 | resp, err := http.DefaultClient.Do(req) 284 | if resp.StatusCode >= http.StatusBadRequest { 285 | err = errors.New(resp.Status) 286 | } 287 | 288 | if err != nil { 289 | b.reportError(err, map[string]string{"url": url}) 290 | } 291 | } 292 | } 293 | } 294 | 295 | func (b *Bot) createTables(tables ...interface{}) (err error) { 296 | for _, t := range tables { 297 | if b.dev() { 298 | err = b.PG.DropTable(t, &orm.DropTableOptions{IfExists: true}) 299 | if err != nil { 300 | return 301 | } 302 | } 303 | 304 | err = b.PG.CreateTable(t, &orm.CreateTableOptions{IfNotExists: true}) 305 | if err != nil { 306 | return 307 | } 308 | } 309 | 310 | return 311 | } 312 | 313 | func (b *Bot) reportError(err error, tags map[string]string) { 314 | if err == nil { 315 | return 316 | } 317 | 318 | if b.prod() { 319 | b.Sentry.CaptureError(err, tags) 320 | return 321 | } 322 | 323 | fmt.Printf("Reported error: %v\n", err) 324 | 325 | var names []string 326 | for name := range tags { 327 | names = append(names, name) 328 | } 329 | 330 | sort.Strings(names) 331 | 332 | for _, name := range names { 333 | fmt.Printf("%v: %v\n", name, tags[name]) 334 | } 335 | } 336 | 337 | func (b *Bot) capturePanic(f func(), tags map[string]string) { 338 | defer func() { 339 | switch err := recover().(type) { 340 | case nil: 341 | return 342 | case error: 343 | b.reportError(err, tags) 344 | default: 345 | b.reportError(fmt.Errorf("%v", err), tags) 346 | } 347 | }() 348 | 349 | f() 350 | } 351 | 352 | func (b *Bot) dev() bool { 353 | return b.opts.Mode == "dev" 354 | } 355 | 356 | func (b *Bot) prod() bool { 357 | return b.opts.Mode == "prod" 358 | } 359 | -------------------------------------------------------------------------------- /bot/commandler/command.go: -------------------------------------------------------------------------------- 1 | package commandler 2 | 3 | // Command represents a command 4 | type Command struct { 5 | Run func(*Context) error 6 | Name string 7 | Info string 8 | Usage string 9 | Aliases []string 10 | GuildOnly bool 11 | OwnerOnly bool 12 | ClientPerms int 13 | MemberPerms int 14 | } 15 | -------------------------------------------------------------------------------- /bot/commandler/commandler.go: -------------------------------------------------------------------------------- 1 | package commandler 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/xdimgg/starboard/bot/localization" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | "github.com/xdimgg/starboard/bot/util" 12 | ) 13 | 14 | // Commandler represents a command handler 15 | type Commandler struct { 16 | Commands []*Command 17 | OwnerID string 18 | commandMap map[string]*Command 19 | onError func(*Context, error, bool) 20 | mu *sync.Mutex 21 | settings Settings 22 | locales *localization.Locales 23 | reMention *regexp.Regexp 24 | } 25 | 26 | // Settings interface 27 | type Settings interface { 28 | GetString(string, string) string 29 | } 30 | 31 | // New creates a new commandler instance 32 | func New(s *discordgo.Session, locales *localization.Locales, settings Settings) *Commandler { 33 | c := &Commandler{ 34 | Commands: make([]*Command, 0), 35 | commandMap: make(map[string]*Command), 36 | mu: &sync.Mutex{}, 37 | settings: settings, 38 | locales: locales, 39 | reMention: regexp.MustCompile("^<@!?" + util.ParseID(s.Token) + ">"), 40 | } 41 | 42 | s.AddHandler(c.MessageCreate) 43 | 44 | return c 45 | } 46 | 47 | // SetOnError sets an onError function 48 | func (c *Commandler) SetOnError(fn func(*Context, error, bool)) { 49 | c.onError = fn 50 | } 51 | 52 | // AddCommand validates and adds a command to the commands map 53 | func (c *Commandler) AddCommand(cmd *Command) { 54 | if cmd.Name == "" { 55 | panic("Command.Name must be set") 56 | } 57 | 58 | if cmd.Run == nil { 59 | panic("Command.Run must be set") 60 | } 61 | 62 | for _, alias := range cmd.Aliases { 63 | c.commandMap[alias] = cmd 64 | } 65 | 66 | if c.locales != nil { 67 | for _, asset := range c.locales.Assets { 68 | aliases := asset.Translation("commands." + cmd.Name + ".aliases") 69 | name := asset.Translation("commands." + cmd.Name + ".name") 70 | 71 | if aliases != nil { 72 | for _, alias := range aliases.([]interface{}) { 73 | c.commandMap[alias.(string)] = cmd 74 | } 75 | } 76 | 77 | if name != nil { 78 | c.commandMap[name.(string)] = cmd 79 | } 80 | } 81 | } 82 | 83 | c.commandMap[cmd.Name] = cmd 84 | c.Commands = append(c.Commands, cmd) 85 | } 86 | 87 | // FindCommand finds a command by searching for it by its names or aliases 88 | func (c *Commandler) FindCommand(name string) *Command { 89 | c.mu.Lock() 90 | defer c.mu.Unlock() 91 | return c.commandMap[name] 92 | } 93 | 94 | // ParsePrefix extracts the prefix of a message and returns false if no prefix was found 95 | func (c *Commandler) ParsePrefix(m *discordgo.Message) (string, bool) { 96 | if c.settings != nil { 97 | guildPrefix := c.settings.GetString(m.GuildID, "prefix") 98 | if guildPrefix != "" && strings.HasPrefix(strings.ToLower(m.Content), strings.ToLower(guildPrefix)) { 99 | return guildPrefix, true 100 | } 101 | } 102 | 103 | match := c.reMention.FindString(m.Content) 104 | if match != "" { 105 | return match, true 106 | } 107 | 108 | return "", m.GuildID == "" 109 | } 110 | -------------------------------------------------------------------------------- /bot/commandler/context.go: -------------------------------------------------------------------------------- 1 | package commandler 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/bwmarrin/discordgo" 9 | ) 10 | 11 | // Context represents the context of where a command was called 12 | type Context struct { 13 | *discordgo.Message 14 | Args []string 15 | Session *discordgo.Session 16 | Command *Command 17 | Commandler *Commandler 18 | Prefix string 19 | Locale func(string, ...interface{}) string 20 | Language string 21 | 22 | guild *discordgo.Guild 23 | } 24 | 25 | var ( 26 | reChannelMention = regexp.MustCompile(`<#(\d{17,19})>`) 27 | reRoleMention = regexp.MustCompile(`<@&(\d{17,19})>`) 28 | ) 29 | 30 | var everyoneReplacer = strings.NewReplacer( 31 | "@everyone", "@\u200beveryone", 32 | "@here", "@\u200bhere", 33 | ) 34 | 35 | // S gets the phrase for a language asset code and panics if it doesn't exist 36 | func (ctx *Context) S(code string, values ...interface{}) string { 37 | translation := ctx.Locale(code, values...) 38 | 39 | if translation == "" { 40 | panic(errors.New("no translation for " + code)) 41 | } 42 | 43 | return everyoneReplacer.Replace(translation) 44 | } 45 | 46 | // List generates a localized list string 47 | func (ctx *Context) List(code string, values ...string) string { 48 | if len(values) == 1 { 49 | return values[0] 50 | } 51 | 52 | list := ctx.S("list.or.prefix") 53 | 54 | if len(values) == 2 { 55 | return list + ctx.S("list.or.double", "``"+values[0]+"``", "``"+values[1]+"``") 56 | } 57 | 58 | seperator := ctx.S("list.or.seperator") 59 | 60 | for i, value := range values { 61 | list += "``" + value + "``" 62 | 63 | switch i { 64 | case len(values) - 1: 65 | case len(values) - 2: 66 | list += ctx.S("list.or.final_seperator") 67 | default: 68 | list += seperator 69 | } 70 | } 71 | 72 | return list 73 | } 74 | 75 | // SayRaw acts as an alias for ChannelMessageSend 76 | func (ctx *Context) SayRaw(content string) (*discordgo.Message, error) { 77 | return ctx.Session.ChannelMessageSend(ctx.ChannelID, content) 78 | } 79 | 80 | // Say acts as an alias for ChannelMessageSend with localization 81 | func (ctx *Context) Say(code string, values ...interface{}) (*discordgo.Message, error) { 82 | return ctx.SayRaw(ctx.S(code, values...)) 83 | } 84 | 85 | // SayList acts as an alias for ChannelMessageSend with List 86 | func (ctx *Context) SayList(code, extraValue string, values ...string) (*discordgo.Message, error) { 87 | return ctx.Say(code, extraValue, ctx.List(code, values...)) 88 | } 89 | 90 | // Edit edit's a message using localization 91 | func (ctx *Context) Edit(m *discordgo.Message, code string, values ...interface{}) (*discordgo.Message, error) { 92 | return ctx.Session.ChannelMessageEdit(ctx.ChannelID, m.ID, ctx.S(code, values...)) 93 | } 94 | 95 | // Channel returns this messages's channel 96 | func (ctx *Context) Channel() *discordgo.Channel { 97 | c, _ := ctx.Session.State.Channel(ctx.ChannelID) 98 | return c 99 | } 100 | 101 | // Guild returns this messages's guild 102 | func (ctx *Context) Guild() *discordgo.Guild { 103 | if ctx.guild == nil { 104 | ctx.guild, _ = ctx.Session.State.Guild(ctx.GuildID) 105 | } 106 | 107 | return ctx.guild 108 | } 109 | 110 | // VoiceState returns this message author's voice state 111 | func (ctx *Context) VoiceState() *discordgo.VoiceState { 112 | g := ctx.Guild() 113 | if g == nil { 114 | return nil 115 | } 116 | 117 | for _, vs := range g.VoiceStates { 118 | if vs.UserID == ctx.Author.ID { 119 | return vs 120 | } 121 | } 122 | 123 | return nil 124 | } 125 | 126 | // MentionedChannels returns all the mentioned channels in a message 127 | func (ctx *Context) MentionedChannels() (channels []*discordgo.Channel) { 128 | for _, mention := range reChannelMention.FindAllStringSubmatch(ctx.Content, -1) { 129 | channel, err := ctx.Session.State.Channel(mention[1]) 130 | if err == nil && channel.GuildID == ctx.GuildID { 131 | channels = append(channels, channel) 132 | } 133 | } 134 | 135 | return 136 | } 137 | 138 | // MentionedRoles returns all the mentioned roles in a message 139 | func (ctx *Context) MentionedRoles() (roles []*discordgo.Role) { 140 | for _, mention := range reRoleMention.FindAllStringSubmatch(ctx.Content, -1) { 141 | role, err := ctx.Session.State.Role(ctx.GuildID, mention[1]) 142 | if err == nil { 143 | roles = append(roles, role) 144 | } 145 | } 146 | 147 | return 148 | } 149 | -------------------------------------------------------------------------------- /bot/commandler/events.go: -------------------------------------------------------------------------------- 1 | package commandler 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/bwmarrin/discordgo" 8 | "github.com/xdimgg/starboard/bot/util" 9 | ) 10 | 11 | // MessageCreate handles the message create event 12 | func (c *Commandler) MessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { 13 | if m.Author.Bot { 14 | return 15 | } 16 | 17 | prefix, hasPrefix := c.ParsePrefix(m.Message) 18 | if !hasPrefix { 19 | return 20 | } 21 | 22 | splitContent := strings.Split(strings.TrimSpace(m.Content[len(prefix):]), " ") 23 | command := strings.ToLower(splitContent[0]) 24 | if command == "" { 25 | return 26 | } 27 | 28 | cmd := c.FindCommand(command) 29 | if cmd == nil { 30 | return 31 | } 32 | 33 | lang := c.settings.GetString(m.GuildID, "language") 34 | l := c.locales.Language(lang) 35 | 36 | if cmd.GuildOnly && m.GuildID == "" { 37 | s.ChannelMessageSend(m.ChannelID, l("restrictions.guild_only")) 38 | return 39 | } 40 | 41 | if cmd.OwnerOnly && m.Author.ID != c.OwnerID { 42 | s.ChannelMessageSend(m.ChannelID, l("restrictions.owner_only")) 43 | return 44 | } 45 | 46 | if m.GuildID != "" { 47 | myPerms, err := s.State.UserChannelPermissions(s.State.User.ID, m.ChannelID) 48 | if err != nil || myPerms&discordgo.PermissionSendMessages != discordgo.PermissionSendMessages { 49 | return 50 | } 51 | 52 | if cmd.ClientPerms != 0 && myPerms&cmd.ClientPerms != cmd.ClientPerms { 53 | s.ChannelMessageSend(m.ChannelID, l("restrictions.permissions.missing.client", util.GetMissing(myPerms, cmd.ClientPerms, l))) 54 | return 55 | } 56 | 57 | if cmd.MemberPerms != 0 { 58 | memberPerms, err := s.State.UserChannelPermissions(m.Author.ID, m.ChannelID) 59 | if err != nil { 60 | s.ChannelMessageSend(m.ChannelID, l("restrictions.permissions.missing.member.error")) 61 | return 62 | } 63 | 64 | if memberPerms&cmd.MemberPerms != cmd.MemberPerms { 65 | s.ChannelMessageSend(m.ChannelID, l("restrictions.permissions.missing.member", util.GetMissing(memberPerms, cmd.MemberPerms, l))) 66 | return 67 | } 68 | } 69 | } 70 | 71 | ctx := &Context{ 72 | Args: splitContent[1:], 73 | Message: m.Message, 74 | Session: s, 75 | Command: cmd, 76 | Commandler: c, 77 | Prefix: prefix, 78 | Locale: l, 79 | Language: lang, 80 | } 81 | 82 | defer func() { 83 | err := recover() 84 | 85 | switch err.(type) { 86 | case nil: 87 | return 88 | case error: 89 | c.onError(ctx, err.(error), true) 90 | default: 91 | c.onError(ctx, fmt.Errorf("%v", err), true) 92 | } 93 | }() 94 | 95 | err := cmd.Run(ctx) 96 | if err != nil { 97 | c.onError(ctx, err, false) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /bot/commands.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "math" 5 | "regexp" 6 | "runtime" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | humanize "github.com/dustin/go-humanize" 13 | 14 | "github.com/go-pg/pg" 15 | 16 | "github.com/xdimgg/starboard/bot/tables" 17 | "github.com/xdimgg/starboard/bot/util" 18 | 19 | "github.com/bwmarrin/discordgo" 20 | 21 | "github.com/xdimgg/starboard/bot/commandler" 22 | ) 23 | 24 | const ( 25 | maxStarProbability float64 = 5 26 | minStarProbability = 0.000001 27 | ) 28 | 29 | const pageSize = 10 30 | 31 | var ( 32 | reMessageID = regexp.MustCompile(`^(\d{17,19})$|https:\/\/(?:ptb\.|canary\.)discordapp\.com\/channels\/\d{17,19}\/(\d{17,19})\/(\d{17,19})`) 33 | seperatorReplacer = strings.NewReplacer("_", "", "-", "") 34 | 35 | typeToInfo = map[string]struct { 36 | Identifier string 37 | Index int 38 | }{ 39 | "user": {"@", 0}, 40 | "channel": {"#", 1}, 41 | // "role": {"@&", 2}, 42 | } 43 | ) 44 | 45 | func (b *Bot) registerCommands(c *commandler.Commandler) { 46 | for _, cmd := range []*commandler.Command{ 47 | { 48 | Run: b.runPing, 49 | Name: "ping", 50 | }, 51 | { 52 | Run: b.runHelp, 53 | Name: "help", 54 | ClientPerms: discordgo.PermissionEmbedLinks, 55 | }, 56 | { 57 | Run: b.runConfig, 58 | Name: "config", 59 | GuildOnly: true, 60 | }, 61 | { 62 | Run: b.runSetup, 63 | Name: "setup", 64 | GuildOnly: true, 65 | ClientPerms: discordgo.PermissionManageChannels, 66 | MemberPerms: discordgo.PermissionManageChannels, 67 | }, 68 | { 69 | Run: b.runStats, 70 | Name: "stats", 71 | ClientPerms: discordgo.PermissionEmbedLinks, 72 | }, 73 | { 74 | Run: b.runInvite, 75 | Name: "invite", 76 | }, 77 | { 78 | Run: b.runBlock, 79 | Name: "block", 80 | GuildOnly: true, 81 | ClientPerms: discordgo.PermissionEmbedLinks, 82 | }, 83 | { 84 | Run: b.runFix, 85 | Name: "fix", 86 | GuildOnly: true, 87 | }, 88 | { 89 | Run: b.runReloadLocales, 90 | Name: "reload-locales", 91 | OwnerOnly: true, 92 | }, 93 | { 94 | Run: b.runLeaderboard, 95 | Name: "leaderboard", 96 | GuildOnly: true, 97 | ClientPerms: discordgo.PermissionEmbedLinks, 98 | }, 99 | { 100 | Run: b.runTroubleshoot, 101 | Name: "troubleshoot", 102 | GuildOnly: true, 103 | }, 104 | } { 105 | c.AddCommand(cmd) 106 | } 107 | } 108 | 109 | func (b *Bot) runPing(ctx *commandler.Context) (err error) { 110 | ms := time.Now().UnixNano() 111 | 112 | sent, err := ctx.Say("commands.ping.phrases.pinging") 113 | if err != nil { 114 | return 115 | } 116 | 117 | _, err = ctx.Edit(sent, "commands.ping.phrases.done", (time.Now().UnixNano()-ms)/int64(time.Millisecond)) 118 | return 119 | } 120 | 121 | func (b *Bot) runHelp(ctx *commandler.Context) (err error) { 122 | names := make([]string, 0) 123 | for _, c := range ctx.Commandler.Commands { 124 | if c.OwnerOnly && ctx.Author.ID != ctx.Commandler.OwnerID { 125 | continue 126 | } 127 | 128 | names = append(names, c.Name) 129 | } 130 | sort.Strings(names) 131 | 132 | var sb strings.Builder 133 | asset := b.Locales.Asset(ctx.Language) 134 | 135 | for _, name := range names { 136 | sb.WriteString(util.EscapeMarkdown(ctx.Prefix)) 137 | if strings.HasPrefix(ctx.Prefix, "<@") { 138 | sb.WriteByte(' ') 139 | } 140 | 141 | sb.WriteString(ctx.S("commands." + name + ".name")) 142 | 143 | usage := asset.Translation("commands." + name + ".usage") 144 | if usage != nil { 145 | sb.WriteString(" " + usage.(string)) 146 | } 147 | 148 | sb.WriteByte('\n') 149 | 150 | sb.WriteString(ctx.S("commands." + name + ".description")) 151 | sb.WriteByte('\n') 152 | 153 | // resource := asset.Translation("commands." + name + ".aliases") 154 | // if resource != nil { 155 | // sb.WriteString("- " + ctx.S("commands.help.phrase.aliases") + ": ") 156 | 157 | // aliases := resource.([]interface{}) 158 | // strs := make([]string, len(aliases)) 159 | // for i, alias := range aliases { 160 | // strs[i] = alias.(string) 161 | // } 162 | 163 | // sb.WriteString(strings.Join(strs, ", ")) 164 | // sb.WriteByte('\n') 165 | // } 166 | 167 | sb.WriteByte('\n') 168 | } 169 | 170 | ctx.Session.ChannelMessageSendEmbed(ctx.ChannelID, &discordgo.MessageEmbed{ 171 | Color: gray, 172 | Description: sb.String(), 173 | Title: ctx.S("commands.help.phrase.commands"), 174 | }) 175 | return 176 | } 177 | 178 | func (b *Bot) runConfig(ctx *commandler.Context) (err error) { 179 | if len(ctx.Args) == 0 { 180 | content := "" 181 | for k, v := range b.Settings.GetID(ctx.GuildID) { 182 | if strings.Contains(k, "channel") && v == settingNone { 183 | if ch := findDefaultChannel(k, ctx.Session.State, ctx.Guild()); ch != nil { 184 | v = ch.ID 185 | } 186 | } 187 | 188 | content += ctx.S("settings."+k) + ": " + getSettingString(k, v) + "\n" 189 | } 190 | ctx.SayRaw(content) 191 | return 192 | } 193 | 194 | key := ctx.Locale("settings.to_key." + seperatorReplacer.Replace(strings.ToLower(ctx.Args[0]))) 195 | if key == "" { 196 | ctx.Say("settings.phrase.unknown") 197 | return 198 | } 199 | 200 | if len(ctx.Args) == 1 { 201 | str := getSettingString(key, b.Settings.Get(ctx.GuildID, key)) 202 | 203 | if strings.Contains(key, settingChannel) && str == settingNone { 204 | if ch := findDefaultChannel(key, ctx.Session.State, ctx.Guild()); ch != nil { 205 | str = "<#" + ch.ID + ">" 206 | } 207 | } 208 | 209 | ctx.SayRaw(ctx.S("settings."+key) + ": " + str) 210 | return 211 | } 212 | 213 | arg := strings.Join(ctx.Args[1:], " ") 214 | l := ctx.S("settings." + key) 215 | var value interface{} 216 | 217 | memberPerms, err := ctx.Session.State.UserChannelPermissions(ctx.Author.ID, ctx.ChannelID) 218 | if err != nil { 219 | ctx.Say("restrictions.permissions.missing.member.error") 220 | return 221 | } 222 | 223 | if memberPerms&discordgo.PermissionManageMessages == 0 { 224 | ctx.Say("restrictions.permissions.missing.member", "```diff\n- "+ctx.S("permissions.MANAGE_MESSAGES")+"\n```") 225 | return 226 | } 227 | 228 | switch key { 229 | case settingPrefix: 230 | if len(arg) > 50 { 231 | ctx.Say("settings.restrictions.max_length", l, 50) 232 | return 233 | } 234 | 235 | value = arg 236 | case settingLanguage: 237 | if code, ok := util.LanguagesReversed[strings.ToLower(arg)]; ok { 238 | arg = code 239 | } 240 | 241 | if b.Locales.Asset(arg) == nil { 242 | var langs []string 243 | for lang := range b.Locales.Assets { 244 | langs = append(langs, util.Languages[lang]) 245 | } 246 | 247 | ctx.SayList("settings.restrictions.one_of", l, langs...) 248 | return 249 | } 250 | 251 | ctx.Locale = b.Locales.Language(arg) 252 | value = arg 253 | case settingMinimum: 254 | i, err := strconv.Atoi(arg) 255 | if err != nil { 256 | ctx.Say("settings.restrictions.number", l) 257 | return nil 258 | } 259 | if i < 1 { 260 | ctx.Say("settings.restrictions.min", l, 1) 261 | return nil 262 | } 263 | if i > 100 { 264 | ctx.Say("settings.restrictions.max", l, 100) 265 | return nil 266 | } 267 | 268 | value = i 269 | case settingSelfStar, settingSelfStarWarning, settingMinimal, settingRemoveBotStars, settingSaveDeletedMessages: 270 | t := ctx.S("settings.phrase.true") 271 | f := ctx.S("settings.phrase.false") 272 | arg = strings.ToLower(arg) 273 | 274 | if arg != t && arg != f { 275 | ctx.SayList("settings.restrictions.one_of", l, t, f) 276 | return 277 | } 278 | 279 | value = arg == t 280 | case settingEmoji: 281 | e := util.ParseEmoji(arg) 282 | if e == nil { 283 | ctx.Say("settings.restrictions.emoji", l) 284 | return 285 | } 286 | 287 | value = e 288 | case settingChannel: 289 | channels := ctx.MentionedChannels() 290 | if len(channels) == 0 || channels[0].Type != discordgo.ChannelTypeGuildText { 291 | ctx.Say("settings.restrictions.channel", l) 292 | return 293 | } 294 | 295 | perms, err := ctx.Session.State.UserChannelPermissions(ctx.Session.State.User.ID, channels[0].ID) 296 | if err != nil || perms&discordgo.PermissionSendMessages != discordgo.PermissionSendMessages { 297 | ctx.Say("settings.restrictions.channel_perms") 298 | return nil 299 | } 300 | 301 | value = channels[0].ID 302 | case settingNSFWChannel: 303 | channels := ctx.MentionedChannels() 304 | if len(channels) == 0 || channels[0].Type != discordgo.ChannelTypeGuildText { 305 | ctx.Say("settings.restrictions.channel", l) 306 | return 307 | } 308 | 309 | perms, err := ctx.Session.State.UserChannelPermissions(ctx.Session.State.User.ID, channels[0].ID) 310 | if err != nil || perms&discordgo.PermissionSendMessages != discordgo.PermissionSendMessages { 311 | ctx.Say("settings.restrictions.channel_perms") 312 | return nil 313 | } 314 | 315 | if !channels[0].NSFW { 316 | ctx.Say("settings.restrictions.channel_nsfw") 317 | return nil 318 | } 319 | 320 | value = channels[0].ID 321 | case settingBlockMode: 322 | b := ctx.S("settings.phrase.blacklist") 323 | w := ctx.S("settings.phrase.whitelist") 324 | arg = strings.ToLower(arg) 325 | 326 | if arg != b && arg != w { 327 | ctx.SayList("settings.restrictions.one_of", l, b, w) 328 | return 329 | } 330 | 331 | if arg == b { 332 | value = "blacklist" 333 | } else { 334 | value = "whitelist" 335 | } 336 | case settingRandomStarProbability: 337 | f, err := strconv.ParseFloat(strings.TrimSuffix(arg, "%"), 64) 338 | if err != nil { 339 | ctx.Say("settings.restrictions.number", l) 340 | return nil 341 | } 342 | if f > maxStarProbability { 343 | ctx.Say("settings.restrictions.max_percentage", l, maxStarProbability) 344 | return nil 345 | } 346 | if f < minStarProbability && f != 0 { 347 | ctx.Say("settings.restrictions.min_percentage", l, minStarProbability) 348 | return nil 349 | } 350 | 351 | value = f 352 | } 353 | 354 | err = b.Settings.Set(ctx.GuildID, key, value) 355 | if err != nil { 356 | return 357 | } 358 | 359 | ctx.Say("settings.phrase.updated", ctx.S("settings."+key)) 360 | return 361 | } 362 | 363 | func (b *Bot) runSetup(ctx *commandler.Context) (err error) { 364 | arg := strings.ToLower(strings.Join(ctx.Args, " ")) 365 | nsfw := strings.Contains(arg, ctx.S("commands.setup.nsfw")) 366 | 367 | setting := settingChannel 368 | if nsfw { 369 | setting = settingNSFWChannel 370 | } 371 | 372 | starboard := b.Settings.GetString(ctx.GuildID, setting) 373 | if starboard == settingNone { 374 | if defCh := findDefaultChannel(setting, ctx.Session.State, ctx.Guild()); defCh != nil { 375 | starboard = defCh.ID 376 | } 377 | } 378 | 379 | if starboard != settingNone { 380 | if _, err = ctx.Session.State.Channel(starboard); err == nil { 381 | ctx.Say("commands.setup.phrase.exists", "<#"+starboard+">") 382 | return 383 | } 384 | } 385 | 386 | name := "starboard" 387 | if nsfw { 388 | name += "-nsfw" 389 | } 390 | 391 | if g := ctx.Guild(); g != nil { 392 | var count int 393 | 394 | for _, c := range g.Channels { 395 | if c.Type != discordgo.ChannelTypeGuildText { 396 | continue 397 | } 398 | 399 | if util.StartsWithEmoji(c.Name) { 400 | count++ 401 | } else { 402 | count-- 403 | } 404 | } 405 | 406 | if count > 0 { 407 | name = starEmoji + name 408 | } 409 | } 410 | 411 | ch, err := ctx.Session.GuildChannelCreateComplex(ctx.GuildID, discordgo.GuildChannelCreateData{ 412 | Type: discordgo.ChannelTypeGuildText, 413 | Name: name, 414 | NSFW: nsfw, 415 | ParentID: ctx.Channel().ParentID, 416 | PermissionOverwrites: []*discordgo.PermissionOverwrite{ 417 | { 418 | ID: ctx.GuildID, 419 | Type: "role", 420 | Deny: discordgo.PermissionSendMessages, 421 | }, 422 | }, 423 | }) 424 | if err != nil { 425 | return 426 | } 427 | 428 | b.Settings.Set(ctx.GuildID, setting, ch.ID) 429 | 430 | ctx.Say("commands.setup.phrase.done", ch.Mention()) 431 | return 432 | } 433 | 434 | func (b *Bot) runStats(ctx *commandler.Context) (err error) { 435 | messageCount, err := b.PG.Model((*tables.Message)(nil)).Count() 436 | if err != nil { 437 | return 438 | } 439 | 440 | starCount, err := b.PG.Model((*tables.Reaction)(nil)).Count() 441 | if err != nil { 442 | return 443 | } 444 | 445 | var m runtime.MemStats 446 | runtime.ReadMemStats(&m) 447 | 448 | var guildCount, channelCount, memberCount int 449 | for _, s := range b.Manager.Sessions { 450 | guildCount += len(s.State.Guilds) 451 | 452 | for _, guild := range s.State.Guilds { 453 | memberCount += guild.MemberCount 454 | channelCount += len(guild.Channels) 455 | } 456 | } 457 | 458 | ctx.Session.ChannelMessageSendEmbed(ctx.ChannelID, &discordgo.MessageEmbed{ 459 | Color: gray, 460 | Fields: []*discordgo.MessageEmbedField{ 461 | { 462 | Name: ctx.S("commands.stats.phrase.system"), 463 | Value: ctx.S( 464 | "commands.stats.phrase.system_value", 465 | time.Since(b.StartTime), 466 | humanize.Bytes(m.Alloc), 467 | humanize.Comma(int64(runtime.NumCPU())), 468 | strings.TrimPrefix(runtime.Version(), "go"), 469 | discordgo.VERSION, 470 | ), 471 | }, 472 | { 473 | Name: ctx.S("commands.stats.phrase.bot"), 474 | Value: ctx.S( 475 | "commands.stats.phrase.bot_value", 476 | humanize.Comma(int64(len(b.Manager.Sessions))), 477 | humanize.Comma(int64(guildCount)), 478 | humanize.Comma(int64(channelCount)), 479 | humanize.Comma(int64(memberCount)), 480 | ), 481 | }, 482 | { 483 | Name: ctx.S("commands.stats.phrase.starboard"), 484 | Value: ctx.S( 485 | "commands.stats.phrase.starboard_value", 486 | humanize.Comma(int64(messageCount)), 487 | humanize.Comma(int64(starCount)), 488 | ), 489 | }, 490 | }, 491 | }) 492 | return 493 | } 494 | 495 | func (b *Bot) runInvite(ctx *commandler.Context) (err error) { 496 | ctx.Session.ChannelMessageSendEmbed(ctx.ChannelID, &discordgo.MessageEmbed{ 497 | Color: gray, 498 | Description: ctx.S( 499 | "commands.invite.phrase.content", 500 | "https://discordapp.com/oauth2/authorize?client_id="+ctx.Session.State.User.ID+"&scope=bot&permissions=8", 501 | "https://discord.gg/MZCKAtF", 502 | "https://www.patreon.com/starboard", 503 | "https://www.paypal.me/DimGG", 504 | "https://discordbots.org/bot/349626729226305537", 505 | ), 506 | }) 507 | return 508 | } 509 | 510 | func (b *Bot) runBlock(ctx *commandler.Context) (err error) { 511 | if len(ctx.Args) != 0 { 512 | memberPerms, err := ctx.Session.State.UserChannelPermissions(ctx.Author.ID, ctx.ChannelID) 513 | if err != nil { 514 | ctx.Say("restrictions.permissions.missing.member.error") 515 | return err 516 | } 517 | 518 | if memberPerms&discordgo.PermissionManageMessages == 0 { 519 | ctx.Say("restrictions.permissions.missing.member", "```diff\n- "+ctx.S("permissions.MANAGE_MESSAGES")+"\n```") 520 | return nil 521 | } 522 | 523 | if len(ctx.Args) == 1 { 524 | ctx.Say("commands.block.phrase.missing") 525 | return nil 526 | } 527 | 528 | action := strings.ToLower(ctx.Args[0]) 529 | add := ctx.S("commands.block.phrase.add") 530 | remove := ctx.S("commands.block.phrase.remove") 531 | all := ctx.S("commands.block.phrase.all") 532 | 533 | if action != add && action != remove { 534 | ctx.SayList("settings.restrictions.one_of", ctx.S("commands.block.phrase.action"), add, remove) 535 | return nil 536 | } 537 | 538 | blocks := make([]tables.Block, 0) 539 | 540 | for _, m := range ctx.Mentions { 541 | blocks = append(blocks, tables.Block{ 542 | ID: m.ID, 543 | GuildID: ctx.GuildID, 544 | Type: "user", 545 | }) 546 | } 547 | 548 | for _, c := range ctx.MentionedChannels() { 549 | blocks = append(blocks, tables.Block{ 550 | ID: c.ID, 551 | GuildID: ctx.GuildID, 552 | Type: "channel", 553 | }) 554 | } 555 | 556 | // for _, r := range ctx.MentionedRoles() { 557 | // blocks = append(blocks, tables.Block{ 558 | // ID: r.ID, 559 | // GuildID: ctx.GuildID, 560 | // Type: "role", 561 | // }) 562 | // } 563 | 564 | if len(blocks) == 0 && (action != remove || ctx.Args[1] != all) { 565 | ctx.Say("commands.block.phrase.missing") 566 | return nil 567 | } 568 | 569 | if action == add { 570 | _, err = b.PG.Model(&blocks).OnConflict("DO NOTHING").Insert() 571 | } else { 572 | q := b.PG.Model((*tables.Block)(nil)).Where("guild_id = ?", ctx.GuildID) 573 | 574 | if ctx.Args[1] != all { 575 | ids := make([]string, len(blocks)) 576 | for _, b := range blocks { 577 | ids = append(ids, b.ID) 578 | } 579 | 580 | q = q.WhereIn("id IN ?", ids) 581 | } 582 | 583 | _, err = q.Delete() 584 | } 585 | 586 | if err != nil { 587 | if err == pg.ErrNoRows { 588 | return nil 589 | } 590 | 591 | if e, ok := err.(pg.Error); ok && e.IntegrityViolation() { 592 | return nil 593 | } 594 | 595 | return err 596 | } 597 | } 598 | 599 | none := ctx.S("commands.block.phrase.none") 600 | embed := &discordgo.MessageEmbed{ 601 | Color: gray, 602 | Description: ctx.S("settings.phrase.mode", ctx.S("settings.phrase."+b.Settings.GetString(ctx.GuildID, settingBlockMode))), 603 | Fields: []*discordgo.MessageEmbedField{ 604 | { 605 | Name: ctx.S("commands.block.phrase.users"), 606 | Value: none, 607 | }, 608 | { 609 | Name: ctx.S("commands.block.phrase.channels"), 610 | Value: none, 611 | }, 612 | // { 613 | // Name: ctx.S("commands.block.phrase.roles"), 614 | // Value: none, 615 | // }, 616 | }, 617 | } 618 | 619 | var blocks []tables.Block 620 | err = b.PG.Model(&blocks).Where("guild_id = ?", ctx.GuildID).Select() 621 | if err != nil && err != pg.ErrNoRows { 622 | return 623 | } 624 | 625 | for _, b := range blocks { 626 | info := typeToInfo[b.Type] 627 | text := "<" + info.Identifier + b.ID + ">\n" 628 | field := embed.Fields[info.Index] 629 | 630 | if field.Value == none { 631 | field.Value = text 632 | } else { 633 | field.Value += text 634 | } 635 | } 636 | 637 | ctx.Session.ChannelMessageSendEmbed(ctx.ChannelID, embed) 638 | return 639 | } 640 | 641 | func (b *Bot) runFix(ctx *commandler.Context) (err error) { 642 | if len(ctx.Args) == 0 { 643 | ctx.Say("commands.fix.phrase.id") 644 | return 645 | } 646 | 647 | matches := reMessageID.FindStringSubmatch(ctx.Args[0]) 648 | if matches == nil { 649 | ctx.Say("commands.fix.phrase.id") 650 | return 651 | } 652 | 653 | channelID := matches[2] 654 | messageID := matches[3] 655 | 656 | if channelID == "" { 657 | channelID = ctx.ChannelID 658 | } 659 | 660 | if messageID == "" { 661 | messageID = matches[1] 662 | } 663 | 664 | required := discordgo.PermissionReadMessages | discordgo.PermissionReadMessageHistory 665 | 666 | perms, err := ctx.Session.State.UserChannelPermissions(ctx.Author.ID, channelID) 667 | if err != nil || perms&required != required { 668 | ctx.Say("commands.fix.phrase.permissions") 669 | } 670 | 671 | err = b.updateMessage(ctx.Session, &tables.Message{ 672 | ID: messageID, 673 | ChannelID: channelID, 674 | GuildID: ctx.GuildID, 675 | }) 676 | if err != nil { 677 | if rErr, ok := err.(*discordgo.RESTError); ok && rErr.Message != nil && rErr.Message.Code == discordgo.ErrCodeUnknownMessage { 678 | ctx.Say("commands.fix.phrase.unknown_message") 679 | return nil 680 | } 681 | 682 | return 683 | } 684 | 685 | ctx.Say("commands.fix.phrase.done") 686 | return 687 | } 688 | 689 | func (b *Bot) runReloadLocales(ctx *commandler.Context) (err error) { 690 | err = b.Locales.ReadAll() 691 | if err != nil { 692 | return 693 | } 694 | 695 | ctx.Say("commands.reload-locales.phrase.done") 696 | return 697 | } 698 | 699 | func (b *Bot) runLeaderboard(ctx *commandler.Context) (err error) { 700 | var dataTotal struct { 701 | Count int 702 | } 703 | _, err = b.PG.Query(&dataTotal, ` 704 | SELECT COUNT(*) AS count FROM ( 705 | SELECT DISTINCT author_id 706 | FROM messages 707 | WHERE guild_id = (?) 708 | ) AS messages 709 | `, ctx.GuildID) 710 | 711 | if dataTotal.Count == 0 { 712 | ctx.Say("commands.leaderboard.phrase.empty") 713 | return 714 | } 715 | 716 | max := int(math.Ceil(float64(dataTotal.Count) / float64(pageSize))) 717 | offset := 0 718 | if len(ctx.Args) != 0 { 719 | i, err := strconv.Atoi(ctx.Args[0]) 720 | if err != nil { 721 | ctx.Say("commands.leaderboard.phrase.number") 722 | return nil 723 | } 724 | 725 | if i < 1 { 726 | ctx.Say("commands.leaderboard.phrase.min", 1) 727 | return nil 728 | } 729 | 730 | if i > max { 731 | ctx.Say("commands.leaderboard.phrase.max", max) 732 | return nil 733 | } 734 | 735 | offset = i - 1 736 | } 737 | 738 | extraWhere := "" 739 | 740 | if !b.Settings.GetBool(ctx.GuildID, settingSelfStar) { 741 | extraWhere += "WHERE messages.author_id != reactions.user_id" 742 | } 743 | 744 | if b.Settings.GetBool(ctx.GuildID, settingRemoveBotStars) { 745 | if extraWhere != "" { 746 | extraWhere += " AND reactions.bot = FALSE" 747 | } 748 | } 749 | 750 | var data []struct { 751 | AuthorID string 752 | TotalStars int 753 | } 754 | _, err = b.PG.Query(&data, ` 755 | SELECT SUM(star_count) AS total_stars, author_id FROM ( 756 | SELECT COUNT(*) AS star_count, author_id, message_id, guild_id FROM reactions 757 | JOIN messages ON messages.id = reactions.message_id 758 | `+extraWhere+` 759 | GROUP BY author_id, message_id, guild_id 760 | ) AS messages 761 | WHERE guild_id = (?) 762 | GROUP BY messages.author_id 763 | ORDER BY total_stars DESC 764 | OFFSET (?) 765 | LIMIT (?) 766 | `, ctx.GuildID, offset*pageSize, pageSize) 767 | if err != nil { 768 | return 769 | } 770 | 771 | embed := &discordgo.MessageEmbed{ 772 | Color: gray, 773 | Fields: []*discordgo.MessageEmbedField{ 774 | { 775 | Name: "User", 776 | Inline: true, 777 | }, 778 | { 779 | Name: "Stars", 780 | Inline: true, 781 | }, 782 | }, 783 | Footer: &discordgo.MessageEmbedFooter{ 784 | Text: ctx.S("commands.leaderboard.phrase.page", offset+1, max), 785 | }, 786 | } 787 | 788 | for i, row := range data { 789 | embed.Fields[0].Value += strconv.Itoa((offset*pageSize)+i+1) + ". <@" + row.AuthorID + ">\n" 790 | embed.Fields[1].Value += strconv.Itoa(row.TotalStars) + "\n" 791 | } 792 | 793 | _, err = ctx.Session.ChannelMessageSendEmbed(ctx.ChannelID, embed) 794 | return 795 | } 796 | 797 | func (b *Bot) runTroubleshoot(ctx *commandler.Context) (err error) { 798 | var errors, warnings []string 799 | 800 | channelData := [...]string{settingChannel, settingNSFWChannel} 801 | 802 | for i, key := range channelData { 803 | channel := b.Settings.GetString(ctx.GuildID, key) 804 | 805 | if channel == settingNone { 806 | if def := findDefaultChannel(key, ctx.Session.State, ctx.Guild()); def != nil { 807 | channel = def.ID 808 | } 809 | } else if _, err := ctx.Session.State.Channel(channel); err != nil { 810 | channel = settingNone 811 | } 812 | 813 | channelData[i] = channel 814 | } 815 | 816 | channel := channelData[0] 817 | nsfwChannel := channelData[1] 818 | 819 | if channel == settingNone && nsfwChannel == settingNone { 820 | errors = append(errors, ctx.S("commands.troubleshoot.missing_channel", ctx.Prefix, ctx.S("commands.setup.name"))) 821 | } else { 822 | var nsfwChannels, channels int 823 | 824 | for _, c := range ctx.Guild().Channels { 825 | if c.Type == discordgo.ChannelTypeGuildText { 826 | if c.NSFW { 827 | nsfwChannels++ 828 | } else { 829 | channels++ 830 | } 831 | } 832 | } 833 | 834 | if channels != 0 && channel == settingNone { 835 | warnings = append(warnings, ctx.S("commands.troubleshoot.missing_channel", ctx.Prefix, ctx.S("commands.setup.name"))) 836 | } 837 | 838 | if nsfwChannels != 0 && nsfwChannel == settingNone { 839 | args := []interface{}{nsfwChannels, ctx.Prefix, ctx.S("commands.setup.name"), ctx.S("commands.setup.nsfw")} 840 | if nsfwChannels == 1 { 841 | warnings = append(warnings, ctx.S("commands.troubleshoot.missing_nsfw_channel", args...)) 842 | } else { 843 | warnings = append(warnings, ctx.S("commands.troubleshoot.missing_nsfw_channel_multiple", args...)) 844 | } 845 | } 846 | } 847 | 848 | for _, id := range []string{channel, nsfwChannel} { 849 | if id == settingNone { 850 | continue 851 | } 852 | 853 | perms, err := ctx.Session.State.UserChannelPermissions(ctx.Session.State.User.ID, id) 854 | if err != nil { 855 | continue 856 | } 857 | 858 | mention := "<#" + id + ">" 859 | 860 | for _, perm := range [...]int{ 861 | discordgo.PermissionSendMessages, 862 | discordgo.PermissionEmbedLinks, 863 | discordgo.PermissionReadMessages, 864 | } { 865 | if perms&perm != perm { 866 | errors = append(errors, ctx.S("commands.troubleshoot.missing_permissions", ctx.S("permissions."+util.Permissions[perm]), mention)) 867 | } 868 | } 869 | } 870 | 871 | if len(errors) == 0 && len(warnings) == 0 { 872 | ctx.Say("commands.troubleshoot.passed", ctx.Prefix, ctx.S("commands.invite.name")) 873 | } else { 874 | var final strings.Builder 875 | 876 | if len(errors) != 0 { 877 | final.WriteString(ctx.S("commands.troubleshoot.errors")) 878 | final.WriteByte('\n') 879 | 880 | for _, e := range errors { 881 | final.WriteString(e) 882 | final.WriteByte('\n') 883 | } 884 | 885 | final.WriteByte('\n') 886 | } 887 | 888 | if len(warnings) != 0 { 889 | final.WriteString(ctx.S("commands.troubleshoot.warnings")) 890 | final.WriteByte('\n') 891 | 892 | for _, warn := range warnings { 893 | final.WriteString(warn) 894 | final.WriteByte('\n') 895 | } 896 | 897 | final.WriteByte('\n') 898 | } 899 | 900 | ctx.SayRaw(final.String()) 901 | } 902 | 903 | return 904 | } 905 | -------------------------------------------------------------------------------- /bot/events.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "time" 7 | 8 | "github.com/go-pg/pg" 9 | 10 | "github.com/xdimgg/starboard/bot/tables" 11 | 12 | "github.com/bwmarrin/discordgo" 13 | "github.com/xdimgg/starboard/bot/util" 14 | ) 15 | 16 | func getIconURL(g *discordgo.Guild) string { 17 | if g.Icon == "" { 18 | return "" 19 | } 20 | 21 | return discordgo.EndpointGuildIcon(g.ID, g.Icon) 22 | } 23 | 24 | func (b *Bot) ready(s *discordgo.Session, r *discordgo.Ready) { 25 | b.expectedGuilds[s] = len(r.Guilds) 26 | s.UpdateStatus(0, "@"+r.User.Username+" help") 27 | } 28 | 29 | func (b *Bot) guildCreate(s *discordgo.Session, g *discordgo.GuildCreate) { 30 | if b.expectedGuilds[s] != 0 { 31 | b.expectedGuilds[s]-- 32 | return 33 | } 34 | 35 | s.ChannelMessageSendEmbed(b.opts.GuildLogChannel, &discordgo.MessageEmbed{ 36 | Author: &discordgo.MessageEmbedAuthor{ 37 | Name: g.Name + " (" + g.ID + ")", 38 | IconURL: getIconURL(g.Guild), 39 | }, 40 | Color: 0x5BFF5B, 41 | Footer: &discordgo.MessageEmbedFooter{Text: "Joined"}, 42 | Timestamp: time.Now().Format(time.RFC3339), 43 | }) 44 | } 45 | 46 | func (b *Bot) guildDelete(s *discordgo.Session, g *discordgo.GuildDelete) { 47 | s.ChannelMessageSendEmbed(b.opts.GuildLogChannel, &discordgo.MessageEmbed{ 48 | Author: &discordgo.MessageEmbedAuthor{ 49 | Name: g.Name + " (" + g.ID + ")", 50 | IconURL: getIconURL(g.Guild), 51 | }, 52 | Color: 0xFF3838, 53 | Footer: &discordgo.MessageEmbedFooter{Text: "Left"}, 54 | Timestamp: time.Now().Format(time.RFC3339), 55 | }) 56 | } 57 | 58 | func (b *Bot) guildMemberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) { 59 | if m.GuildID != b.opts.Guild { 60 | return 61 | } 62 | 63 | s.ChannelMessageSendEmbed(b.opts.MemberLogChannel, &discordgo.MessageEmbed{ 64 | Author: &discordgo.MessageEmbedAuthor{ 65 | Name: m.User.Username + " (" + m.User.ID + ")", 66 | IconURL: m.User.AvatarURL(""), 67 | }, 68 | Color: 0x5BFF5B, 69 | Footer: &discordgo.MessageEmbedFooter{Text: "Joined"}, 70 | Timestamp: time.Now().Format(time.RFC3339), 71 | }) 72 | } 73 | 74 | func (b *Bot) guildMemberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) { 75 | if m.GuildID != b.opts.Guild { 76 | return 77 | } 78 | 79 | s.ChannelMessageSendEmbed(b.opts.MemberLogChannel, &discordgo.MessageEmbed{ 80 | Author: &discordgo.MessageEmbedAuthor{ 81 | Name: m.User.Username + " (" + m.User.ID + ")", 82 | IconURL: m.User.AvatarURL(""), 83 | }, 84 | Color: 0xFF3838, 85 | Footer: &discordgo.MessageEmbedFooter{Text: "Left"}, 86 | Timestamp: time.Now().Format(time.RFC3339), 87 | }) 88 | } 89 | 90 | func (b *Bot) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) (err error) { 91 | if m.GuildID == "" { 92 | return 93 | } 94 | 95 | if err := b.cacheMessage(m.Message); err != nil { 96 | b.reportError(err, map[string]string{"event": "MESSAGE_CREATE"}) 97 | } 98 | 99 | probability := b.Settings.Get(m.GuildID, settingRandomStarProbability).(float64) 100 | if probability == 0 { 101 | return 102 | } 103 | 104 | if rand.Float64() <= (probability / 100) { 105 | var perms int 106 | perms, err = s.State.UserChannelPermissions(s.State.User.ID, m.ChannelID) 107 | if err != nil || perms&discordgo.PermissionAddReactions != discordgo.PermissionAddReactions { 108 | return 109 | } 110 | 111 | err = s.MessageReactionAdd(m.ChannelID, m.ID, b.Settings.GetEmoji(m.GuildID, settingEmoji).API()) 112 | if err != nil { 113 | return 114 | } 115 | } 116 | 117 | return 118 | } 119 | 120 | func (b *Bot) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) (err error) { 121 | if m.GuildID == "" { 122 | return 123 | } 124 | 125 | b.mutexGroup.Lock(m.ID) 126 | defer b.mutexGroup.Unlock(m.ID) 127 | 128 | key := "messages:" + m.ID 129 | 130 | if b.Redis.Exists(key).Val() == 1 { 131 | if m.EditedTimestamp != "" { 132 | err = b.Redis.HMSet(key, map[string]interface{}{ 133 | "content": util.GetContent(m.Message), 134 | "image": util.GetImage(m.Message), 135 | }).Err() 136 | } else if image := util.GetImage(m.Message); image != "" { 137 | err = b.Redis.HSet(key, "image", image).Err() 138 | } else { 139 | return 140 | } 141 | 142 | if err != nil { 143 | return 144 | } 145 | 146 | err = b.Redis.Expire(key, expiryTime).Err() 147 | if err != nil { 148 | return 149 | } 150 | } 151 | 152 | image := util.GetImage(m.Message) 153 | 154 | q := b.PG. 155 | Model((*tables.Message)(nil)). 156 | Where("id = ?", m.ID) 157 | 158 | if m.EditedTimestamp != "" { 159 | q = q.Set("content = ?", m.Content).Set("image = ?", image) 160 | } else if image != "" { 161 | q = q.Set("image = ?", image) 162 | } else { 163 | return 164 | } 165 | 166 | _, err = q.Update() 167 | if err != nil { 168 | return 169 | } 170 | 171 | err = b.updateMessage(s, &tables.Message{ 172 | ID: m.ID, 173 | ChannelID: m.ChannelID, 174 | GuildID: m.GuildID, 175 | }) 176 | if err != nil { 177 | return 178 | } 179 | 180 | return 181 | } 182 | 183 | func (b *Bot) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) (err error) { 184 | if m.GuildID == "" { 185 | return 186 | } 187 | 188 | if b.Settings.GetBool(m.GuildID, settingSaveDeletedMessages) { 189 | return 190 | } 191 | 192 | b.mutexGroup.Lock(m.ID) 193 | defer b.mutexGroup.Unlock(m.ID) 194 | 195 | msg := &tables.Message{ID: m.ID} 196 | err = b.PG.Select(msg) 197 | if err != nil { 198 | if err == pg.ErrNoRows { 199 | return nil 200 | } 201 | return 202 | } 203 | 204 | starboard := b.getStarboard(s, msg.ChannelID, msg.GuildID) 205 | if starboard == settingNone { 206 | return 207 | } 208 | 209 | var wg sync.WaitGroup 210 | wg.Add(2) 211 | 212 | go func() { 213 | s.ChannelMessageDelete(starboard, msg.SentID) 214 | wg.Done() 215 | }() 216 | go func() { 217 | err = b.PG.Delete(msg) 218 | wg.Done() 219 | }() 220 | 221 | wg.Wait() 222 | 223 | return 224 | } 225 | 226 | func (b *Bot) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageDeleteBulk) (err error) { 227 | if m.GuildID == "" { 228 | return 229 | } 230 | 231 | if b.Settings.GetBool(m.GuildID, settingSaveDeletedMessages) { 232 | return 233 | } 234 | 235 | args := make([]interface{}, len(m.Messages)) 236 | 237 | for i, id := range m.Messages { 238 | args[i] = id 239 | b.mutexGroup.Lock(id) 240 | defer b.mutexGroup.Unlock(id) 241 | } 242 | 243 | var rows []tables.Message 244 | err = b.PG.Model(&rows).WhereIn("id IN (?)", args...).Returning("sent_id").Select() 245 | if err != nil { 246 | if err == pg.ErrNoRows { 247 | return nil 248 | } 249 | return 250 | } 251 | 252 | msg := &tables.Message{ 253 | ChannelID: m.ChannelID, 254 | GuildID: m.GuildID, 255 | } 256 | 257 | starboard := b.getStarboard(s, msg.ChannelID, msg.GuildID) 258 | if starboard == settingNone { 259 | return 260 | } 261 | 262 | messages := make([]string, len(rows)) 263 | 264 | for i, row := range rows { 265 | messages[i] = row.SentID 266 | } 267 | 268 | var wg sync.WaitGroup 269 | wg.Add(2) 270 | 271 | go func() { 272 | s.ChannelMessagesBulkDelete(starboard, messages) 273 | wg.Done() 274 | }() 275 | go func() { 276 | _, err = b.PG.Model((*tables.Message)(nil)).WhereIn("id IN (?)", args...).Delete() 277 | wg.Done() 278 | }() 279 | 280 | wg.Wait() 281 | 282 | return 283 | } 284 | 285 | func (b *Bot) messageReactionAdd(s *discordgo.Session, m *discordgo.MessageReactionAdd) (err error) { 286 | if m.GuildID == "" { 287 | return 288 | } 289 | 290 | emoji := b.Settings.GetEmoji(m.GuildID, settingEmoji) 291 | 292 | if m.Emoji.ID == "" { 293 | if m.Emoji.Name != emoji.Unicode { 294 | return 295 | } 296 | } else if m.Emoji.ID != emoji.ID { 297 | return 298 | } 299 | 300 | b.mutexGroup.Lock(m.MessageID) 301 | defer b.mutexGroup.Unlock(m.MessageID) 302 | 303 | member, err := s.State.Member(m.GuildID, m.UserID) 304 | perms, _ := s.State.UserChannelPermissions(s.State.User.ID, m.ChannelID) 305 | bot := false 306 | 307 | if m.UserID != s.State.User.ID { 308 | mm := perms&discordgo.PermissionManageMessages == discordgo.PermissionManageMessages 309 | if err == nil && member.User.Bot { 310 | bot = true 311 | 312 | if mm && b.Settings.GetBool(m.GuildID, settingRemoveBotStars) { 313 | err = s.MessageReactionRemove(m.ChannelID, m.MessageID, m.Emoji.APIName(), m.UserID) 314 | if err == nil { 315 | return 316 | } 317 | } 318 | } else if mm && !b.Settings.GetBool(m.GuildID, settingSelfStar) { 319 | msg, err := b.getMessage(s, m.MessageID, m.ChannelID) 320 | if err != nil { 321 | return err 322 | } 323 | 324 | if msg.AuthorID == m.UserID { 325 | err := s.MessageReactionRemove(m.ChannelID, m.MessageID, m.Emoji.APIName(), m.UserID) 326 | if err == nil { 327 | key := "warned:" + m.UserID 328 | 329 | if b.Redis.Exists(key).Val() == 0 && b.Settings.GetBool(m.GuildID, settingSelfStarWarning) { 330 | b.Redis.Set(key, "", time.Hour) 331 | l := b.Locales.Language(b.Settings.GetString(msg.GuildID, settingLanguage)) 332 | s.ChannelMessageSend(m.ChannelID, l("starboard.self_star.warning", "<@"+m.UserID+">")) 333 | } 334 | } 335 | } 336 | } 337 | } 338 | 339 | _, err = b.PG.Model(&tables.Reaction{ 340 | Bot: bot, 341 | UserID: m.UserID, 342 | MessageID: m.MessageID, 343 | }).OnConflict("DO NOTHING").Insert() 344 | if err != nil { 345 | return 346 | } 347 | 348 | return b.updateMessage(s, &tables.Message{ 349 | ID: m.MessageID, 350 | ChannelID: m.ChannelID, 351 | GuildID: m.GuildID, 352 | }) 353 | } 354 | 355 | func (b *Bot) messageReactionRemove(s *discordgo.Session, m *discordgo.MessageReactionRemove) (err error) { 356 | if m.GuildID == "" { 357 | return 358 | } 359 | 360 | emoji := b.Settings.GetEmoji(m.GuildID, settingEmoji) 361 | 362 | if m.Emoji.ID == "" { 363 | if m.Emoji.Name != emoji.Unicode { 364 | return 365 | } 366 | } else if m.Emoji.ID != emoji.ID { 367 | return 368 | } 369 | 370 | b.mutexGroup.Lock(m.MessageID) 371 | defer b.mutexGroup.Unlock(m.MessageID) 372 | 373 | err = b.PG.Delete(&tables.Reaction{ 374 | UserID: m.UserID, 375 | MessageID: m.MessageID, 376 | }) 377 | if err != nil && err != pg.ErrNoRows { 378 | return 379 | } 380 | 381 | err = b.updateMessage(s, &tables.Message{ 382 | ID: m.MessageID, 383 | ChannelID: m.ChannelID, 384 | GuildID: m.GuildID, 385 | }) 386 | if err != nil { 387 | if e, ok := err.(pg.Error); ok && e.IntegrityViolation() { 388 | return 389 | } 390 | return 391 | } 392 | 393 | return 394 | } 395 | 396 | func (b *Bot) messageReactionRemoveAll(s *discordgo.Session, m *discordgo.MessageReactionRemoveAll) (err error) { 397 | if m.GuildID == "" { 398 | return 399 | } 400 | 401 | b.mutexGroup.Lock(m.MessageID) 402 | defer b.mutexGroup.Unlock(m.MessageID) 403 | 404 | msg := &tables.Message{ID: m.MessageID} 405 | err = b.PG.Select(msg) 406 | if err != nil { 407 | if err == pg.ErrNoRows { 408 | return nil 409 | } 410 | return 411 | } 412 | 413 | starboard := b.getStarboard(s, msg.ChannelID, msg.GuildID) 414 | if starboard == settingNone { 415 | return 416 | } 417 | 418 | var wg sync.WaitGroup 419 | wg.Add(2) 420 | 421 | go func() { 422 | s.ChannelMessageDelete(starboard, msg.SentID) 423 | wg.Done() 424 | }() 425 | go func() { 426 | err = b.PG.Delete(msg) 427 | wg.Done() 428 | }() 429 | 430 | wg.Wait() 431 | 432 | return 433 | } 434 | -------------------------------------------------------------------------------- /bot/localization/localization.go: -------------------------------------------------------------------------------- 1 | package localization 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | // Asset represents a language's assets 14 | type Asset struct { 15 | sync.Mutex 16 | Translations map[string]interface{} 17 | } 18 | 19 | // Locales stores all assets 20 | type Locales struct { 21 | sync.Mutex 22 | Assets map[string]*Asset 23 | dir string 24 | enUS *Asset 25 | } 26 | 27 | // New creates a new Locales from a directory 28 | func New(dir string) (l *Locales, err error) { 29 | l = &Locales{ 30 | Assets: make(map[string]*Asset), 31 | dir: dir, 32 | } 33 | 34 | return l, l.ReadAll() 35 | } 36 | 37 | // ReadAll reads all the translations in the provided directory 38 | func (l *Locales) ReadAll() (err error) { 39 | 40 | files, err := ioutil.ReadDir(l.dir) 41 | if err != nil { 42 | return 43 | } 44 | 45 | var final error 46 | var wg sync.WaitGroup 47 | wg.Add(len(files)) 48 | 49 | for _, file := range files { 50 | go func(file os.FileInfo) { 51 | defer wg.Done() 52 | 53 | ext := filepath.Ext(file.Name()) 54 | if ext != ".json" { 55 | return 56 | } 57 | 58 | content, err := ioutil.ReadFile(filepath.Join(l.dir, file.Name())) 59 | if err != nil { 60 | final = err 61 | return 62 | } 63 | 64 | a := &Asset{ 65 | Translations: make(map[string]interface{}), 66 | } 67 | err = json.Unmarshal(content, &a.Translations) 68 | if err != nil { 69 | final = err 70 | return 71 | } 72 | 73 | l.Lock() 74 | l.Assets[strings.TrimSuffix(file.Name(), ext)] = a 75 | l.Unlock() 76 | }(file) 77 | } 78 | 79 | wg.Wait() 80 | 81 | l.enUS = l.Asset("en-US") 82 | 83 | return final 84 | } 85 | 86 | // Asset gets an asset by its code 87 | func (l *Locales) Asset(code string) *Asset { 88 | l.Lock() 89 | defer l.Unlock() 90 | return l.Assets[code] 91 | } 92 | 93 | // Translation gets the translation for the specified resource 94 | func (a *Asset) Translation(resource string) interface{} { 95 | a.Lock() 96 | defer a.Unlock() 97 | return a.Translations[resource] 98 | } 99 | 100 | // Language returns a function that gets a translation for that string and falls back to english 101 | func (l *Locales) Language(code string) func(string, ...interface{}) string { 102 | a := l.Asset(code) 103 | 104 | return func(resource string, values ...interface{}) string { 105 | if a == nil { 106 | t := l.enUS.Translation(resource) 107 | if t == nil { 108 | return "" 109 | } 110 | 111 | return fmt.Sprintf(t.(string), values...) 112 | } 113 | 114 | translation := a.Translation(resource) 115 | 116 | if translation == nil { 117 | t := l.enUS.Translation(resource) 118 | if t == nil { 119 | return "" 120 | } 121 | 122 | return fmt.Sprintf(t.(string), values...) 123 | } 124 | 125 | return fmt.Sprintf(translation.(string), values...) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /bot/settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/xdimgg/starboard/bot/util" 7 | 8 | "github.com/go-pg/pg" 9 | "github.com/go-pg/pg/orm" 10 | ) 11 | 12 | // Settings represents the settings struct 13 | type Settings struct { 14 | db *pg.DB 15 | mu *sync.RWMutex 16 | cache map[string]*sync.Map 17 | Defaults *sync.Map 18 | } 19 | 20 | // Setting represents a setting database entry 21 | type Setting struct { 22 | ID string `sql:",pk"` 23 | Key string `sql:",pk"` 24 | Value string 25 | } 26 | 27 | // New creates a new settings instance 28 | func New(db *pg.DB, defaults map[string]interface{}) (s *Settings, err error) { 29 | s = &Settings{ 30 | db: db, 31 | mu: &sync.RWMutex{}, 32 | cache: make(map[string]*sync.Map), 33 | Defaults: &sync.Map{}, 34 | } 35 | 36 | for key, value := range defaults { 37 | s.Defaults.Store(key, value) 38 | } 39 | 40 | err = s.db.CreateTable((*Setting)(nil), &orm.CreateTableOptions{IfNotExists: true}) 41 | if err != nil { 42 | return 43 | } 44 | 45 | var settings []Setting 46 | err = s.db.Model(&settings).Select() 47 | if err != nil { 48 | return 49 | } 50 | 51 | for _, row := range settings { 52 | if _, ok := s.cache[row.ID]; !ok { 53 | s.cache[row.ID] = &sync.Map{} 54 | } 55 | 56 | s.cache[row.ID].Store(row.Key, deserialize(row.Value)) 57 | } 58 | 59 | return 60 | } 61 | 62 | // Get gets a setting 63 | func (s *Settings) Get(id, key string) interface{} { 64 | s.mu.RLock() 65 | cache, ok := s.cache[id] 66 | s.mu.RUnlock() 67 | 68 | if ok { 69 | value, ok := cache.Load(key) 70 | if ok { 71 | return value 72 | } 73 | } 74 | 75 | value, _ := s.Defaults.Load(key) 76 | return value 77 | } 78 | 79 | // GetID gets all the settings of an ID 80 | func (s *Settings) GetID(id string) map[string]interface{} { 81 | s.mu.RLock() 82 | cache, ok := s.cache[id] 83 | s.mu.RUnlock() 84 | 85 | values := make(map[string]interface{}) 86 | 87 | s.Defaults.Range(func(key, value interface{}) bool { 88 | values[key.(string)] = value 89 | return true 90 | }) 91 | 92 | if ok { 93 | cache.Range(func(key, value interface{}) bool { 94 | values[key.(string)] = value 95 | return true 96 | }) 97 | } 98 | 99 | return values 100 | } 101 | 102 | // GetInt gets a setting as an int 103 | func (s *Settings) GetInt(id, key string) int { 104 | return s.Get(id, key).(int) 105 | } 106 | 107 | // GetString gets a setting as a string 108 | func (s *Settings) GetString(id, key string) string { 109 | return s.Get(id, key).(string) 110 | } 111 | 112 | // GetBool gets a setting as a bool 113 | func (s *Settings) GetBool(id, key string) bool { 114 | return s.Get(id, key).(bool) 115 | } 116 | 117 | // GetEmoji gets a setting as an emoji 118 | func (s *Settings) GetEmoji(id, key string) *util.Emoji { 119 | return s.Get(id, key).(*util.Emoji) 120 | } 121 | 122 | // Set sets a setting 123 | func (s *Settings) Set(id, key string, value interface{}) (err error) { 124 | s.mu.RLock() 125 | cache, ok := s.cache[id] 126 | s.mu.RUnlock() 127 | 128 | if !ok { 129 | cache = &sync.Map{} 130 | 131 | s.mu.Lock() 132 | s.cache[id] = cache 133 | s.mu.Unlock() 134 | } 135 | 136 | cache.Store(key, value) 137 | 138 | _, err = s.db. 139 | Model(&Setting{ID: id, Key: key, Value: serialize(value)}). 140 | OnConflict("(id, key) DO UPDATE"). 141 | Set("value = excluded.value"). 142 | Insert() 143 | return 144 | } 145 | -------------------------------------------------------------------------------- /bot/settings/types.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/xdimgg/starboard/bot/util" 9 | ) 10 | 11 | func serialize(v interface{}) string { 12 | switch val := v.(type) { 13 | case int: 14 | return "i" + strconv.Itoa(val) 15 | 16 | case float64: 17 | return "f" + strconv.FormatFloat(val, 'f', -1, 64) 18 | 19 | case bool: 20 | if val { 21 | return "1" 22 | } 23 | 24 | return "0" 25 | 26 | case string: 27 | return "s" + val 28 | 29 | case *util.Emoji: 30 | e := val 31 | var str string 32 | if e.Animated { 33 | str = "a" 34 | } 35 | 36 | return str + "," + e.ID + "," + e.Name + "," + e.Unicode 37 | 38 | default: 39 | panic(fmt.Errorf("Unhandled type: %T", v)) 40 | } 41 | } 42 | 43 | func deserialize(str string) interface{} { 44 | if str == "1" { 45 | return true 46 | } 47 | 48 | if str == "0" { 49 | return false 50 | } 51 | 52 | if str[0] == 'i' { 53 | i, _ := strconv.Atoi(str[1:]) 54 | return i 55 | } 56 | 57 | if str[0] == 'f' { 58 | f, _ := strconv.ParseFloat(str[1:], 64) 59 | return f 60 | } 61 | 62 | if str[0] == 's' { 63 | return str[1:] 64 | } 65 | 66 | split := strings.Split(str, ",") 67 | 68 | return &util.Emoji{ 69 | Animated: len(split[0]) == 1, 70 | ID: split[1], 71 | Name: split[2], 72 | Unicode: split[3], 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /bot/starboard.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "math/rand" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/xdimgg/starboard/bot/util" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | "github.com/go-pg/pg" 12 | "github.com/jinzhu/inflection" 13 | "github.com/xdimgg/starboard/bot/tables" 14 | ) 15 | 16 | const expiryTime = time.Minute * 20 17 | const gray = 0x2E3036 18 | 19 | var styles = [...]struct{ max, color int }{ 20 | {100, 0x6F29CE}, 21 | {50, 0xFFB549}, 22 | {10, 0xFFB13F}, 23 | {0, 0xFFAC33}, 24 | } 25 | 26 | func init() { 27 | rand.Seed(time.Now().UnixNano()) 28 | } 29 | 30 | func (b *Bot) getStarboard(s *discordgo.Session, channelID, guildID string) (starboard string) { 31 | setting := settingChannel 32 | c, err := s.State.Channel(channelID) 33 | if err != nil || c.NSFW { 34 | setting = settingNSFWChannel 35 | } 36 | 37 | starboard = b.Settings.GetString(guildID, setting) 38 | if starboard == settingNone { 39 | g, err := s.State.Guild(guildID) 40 | if err != nil { 41 | return 42 | } 43 | 44 | ch := findDefaultChannel(setting, s.State, g) 45 | if ch == nil { 46 | return 47 | } 48 | 49 | starboard = ch.ID 50 | } 51 | 52 | if starboard == "" { 53 | starboard = settingNone 54 | } 55 | 56 | return 57 | } 58 | 59 | func (b *Bot) generateEmbed(msg *tables.Message, count int) (embed *discordgo.MessageEmbed) { 60 | emoji := b.Settings.GetEmoji(msg.GuildID, settingEmoji) 61 | minimal := b.Settings.GetBool(msg.GuildID, settingMinimal) 62 | s := b.Locales.Language(b.Settings.GetString(msg.GuildID, settingLanguage)) 63 | 64 | embed = &discordgo.MessageEmbed{ 65 | Author: &discordgo.MessageEmbedAuthor{ 66 | Name: s("message.content"), 67 | URL: "https://discordapp.com/channels/" + msg.GuildID + "/" + msg.ChannelID + "/" + msg.ID, 68 | }, 69 | Color: gray, 70 | Description: msg.Content, 71 | Fields: []*discordgo.MessageEmbedField{ 72 | { 73 | Name: s("message.author"), 74 | Value: "<@" + msg.AuthorID + ">", 75 | Inline: true, 76 | }, 77 | { 78 | Name: s("message.channel"), 79 | Value: "<#" + msg.ChannelID + ">", 80 | Inline: true, 81 | }, 82 | }, 83 | } 84 | 85 | if emoji.ID == "" { 86 | embed.Footer = &discordgo.MessageEmbedFooter{ 87 | Text: emoji.Unicode + " " + strconv.Itoa(count), 88 | } 89 | } else { 90 | embed.Footer = &discordgo.MessageEmbedFooter{ 91 | Text: strconv.Itoa(count), 92 | IconURL: emoji.URL(), 93 | } 94 | } 95 | 96 | if !minimal { 97 | if count != 1 { 98 | emoji.Name = inflection.Plural(emoji.Name) 99 | } 100 | 101 | embed.Footer.Text += " " + emoji.Name 102 | embed.Timestamp = util.SnowflakeTimestamp(msg.ID).Format(time.RFC3339) 103 | 104 | for _, style := range styles { 105 | if count >= style.max { 106 | embed.Color = style.color 107 | break 108 | } 109 | } 110 | } 111 | 112 | if msg.Image != "" { 113 | embed.Image = &discordgo.MessageEmbedImage{ 114 | URL: msg.Image, 115 | } 116 | } 117 | 118 | return 119 | } 120 | 121 | func (b *Bot) getMessage(s *discordgo.Session, id, channel string) (msg *tables.Message, err error) { 122 | key := "messages:" + id 123 | res := b.Redis.HMGet(key, "author_id", "guild_id", "content", "image") 124 | if res.Err() != nil { 125 | return nil, res.Err() 126 | } 127 | 128 | data := res.Val() 129 | 130 | if data[0] == nil { 131 | m, err := s.ChannelMessage(channel, id) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | c, err := s.State.Channel(m.ChannelID) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | msg = &tables.Message{ 142 | ID: id, 143 | AuthorID: m.Author.ID, 144 | ChannelID: m.ChannelID, 145 | GuildID: c.GuildID, 146 | 147 | Content: util.GetContent(m), 148 | Image: util.GetImage(m), 149 | } 150 | 151 | completeCount, err := b.countStars(msg, true) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | emoji := b.Settings.GetEmoji(m.GuildID, settingEmoji) 157 | for _, r := range m.Reactions { 158 | if r.Emoji.ID == "" { 159 | if r.Emoji.Name != emoji.Unicode { 160 | continue 161 | } 162 | } else if r.Emoji.ID != emoji.ID { 163 | continue 164 | } 165 | 166 | if completeCount == r.Count { 167 | break 168 | } 169 | 170 | _, err = b.PG.Model((*tables.Reaction)(nil)).Where("message_id = ?", m.ID).Delete() 171 | if err != nil && err != pg.ErrNoRows { 172 | return nil, err 173 | } 174 | 175 | after := "" 176 | reactions := make([]tables.Reaction, 0) 177 | 178 | for len(reactions) < r.Count { 179 | users, err := s.MessageReactions(m.ChannelID, m.ID, r.Emoji.APIName(), 100, "", after) 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | for _, u := range users { 185 | reactions = append(reactions, tables.Reaction{ 186 | Bot: u.Bot, 187 | UserID: u.ID, 188 | MessageID: m.ID, 189 | }) 190 | } 191 | 192 | if len(users) != 0 { 193 | after = users[len(users)-1].ID 194 | } 195 | } 196 | 197 | if len(reactions) != 0 { 198 | _, err = b.PG.Model(&reactions).OnConflict("DO NOTHING").Insert() 199 | if err != nil { 200 | return nil, err 201 | } 202 | } 203 | 204 | break 205 | } 206 | 207 | go b.cacheMessage(m) 208 | } else { 209 | msg = &tables.Message{ 210 | ID: id, 211 | AuthorID: data[0].(string), 212 | ChannelID: channel, 213 | GuildID: data[1].(string), 214 | 215 | Content: data[2].(string), 216 | Image: data[3].(string), 217 | } 218 | } 219 | 220 | return 221 | } 222 | 223 | func (b *Bot) cacheMessage(m *discordgo.Message) (err error) { 224 | key := "messages:" + m.ID 225 | 226 | err = b.Redis.HMSet(key, map[string]interface{}{ 227 | "author_id": m.Author.ID, 228 | "channel_id": m.ChannelID, 229 | "guild_id": m.GuildID, 230 | 231 | "content": util.GetContent(m), 232 | "image": util.GetImage(m), 233 | }).Err() 234 | if err != nil { 235 | return 236 | } 237 | 238 | return b.Redis.Expire(key, expiryTime).Err() 239 | } 240 | 241 | func (b *Bot) createMessage(s *discordgo.Session, m *tables.Message) (err error) { 242 | m, err = b.getMessage(s, m.ID, m.ChannelID) 243 | if err != nil { 244 | return 245 | } 246 | 247 | c, _ := b.PG. 248 | Model((*tables.Block)(nil)). 249 | Where("guild_id = ?", m.GuildID). 250 | Where("type = 'user' AND id = ?", m.AuthorID). 251 | WhereOr("type = 'channel' AND id = ?", m.ChannelID). 252 | Count() 253 | 254 | switch b.Settings.GetString(m.GuildID, settingBlockMode) { 255 | case "blacklist": 256 | if c != 0 { 257 | return 258 | } 259 | 260 | case "whitelist": 261 | if c == 0 { 262 | return 263 | } 264 | } 265 | 266 | starboard := b.getStarboard(s, m.ChannelID, m.GuildID) 267 | if starboard == settingNone { 268 | return 269 | } 270 | 271 | count, err := b.countStars(m, false) 272 | if err != nil { 273 | return 274 | } 275 | 276 | if count < b.Settings.GetInt(m.GuildID, settingMinimum) { 277 | return 278 | } 279 | 280 | sent, err := s.ChannelMessageSendEmbed(starboard, b.generateEmbed(m, count)) 281 | if err != nil { 282 | return 283 | } 284 | 285 | m.SentID = sent.ID 286 | err = b.PG.Insert(m) 287 | return 288 | } 289 | 290 | func (b *Bot) countStars(m *tables.Message, raw bool) (int, error) { 291 | q := b.PG.Model((*tables.Reaction)(nil)).Where("message_id = ?", m.ID) 292 | 293 | if !raw { 294 | if !b.Settings.GetBool(m.GuildID, settingSelfStar) { 295 | q = q.Where("user_id != ?", m.AuthorID) 296 | } 297 | 298 | if b.Settings.GetBool(m.GuildID, settingRemoveBotStars) { 299 | q = q.Where("bot = FALSE") 300 | } 301 | } 302 | 303 | return q.Count() 304 | } 305 | 306 | func (b *Bot) updateMessage(s *discordgo.Session, m *tables.Message) (err error) { 307 | err = b.PG.Select(m) 308 | if err != nil { 309 | if err == pg.ErrNoRows { 310 | return b.createMessage(s, m) 311 | } 312 | 313 | return 314 | } 315 | 316 | count, err := b.countStars(m, false) 317 | if err != nil { 318 | return 319 | } 320 | 321 | starboard := b.getStarboard(s, m.ChannelID, m.GuildID) 322 | if starboard == settingNone { 323 | return 324 | } 325 | 326 | if count < b.Settings.GetInt(m.GuildID, settingMinimum) { 327 | go s.ChannelMessageDelete(starboard, m.SentID) 328 | go b.PG.Delete(m) 329 | return 330 | } 331 | 332 | embed := b.generateEmbed(m, count) 333 | _, err = s.ChannelMessageEditEmbed(starboard, m.SentID, embed) 334 | if err == nil { 335 | return 336 | } 337 | 338 | if rErr, ok := err.(*discordgo.RESTError); ok && rErr.Message != nil { 339 | if rErr.Message.Code != discordgo.ErrCodeUnknownMessage { 340 | return 341 | } 342 | 343 | sent, err := s.ChannelMessageSendEmbed(starboard, embed) 344 | if err != nil { 345 | return err 346 | } 347 | 348 | _, err = b.PG.Model(&tables.Message{ID: m.ID, SentID: sent.ID}).WherePK().UpdateNotNull() 349 | return err 350 | } 351 | 352 | return 353 | } 354 | -------------------------------------------------------------------------------- /bot/tables/tables.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | // Message represents a Discord message 4 | type Message struct { 5 | ID string `sql:",pk"` 6 | AuthorID string 7 | ChannelID string 8 | GuildID string 9 | SentID string 10 | 11 | Content string 12 | Image string 13 | } 14 | 15 | // Reaction represents a Discord reaction 16 | type Reaction struct { 17 | Bot bool `sql:",notnull"` 18 | UserID string `sql:",pk"` 19 | MessageID string `sql:",pk"` 20 | } 21 | 22 | // Block represents a blocker user/channel/role 23 | type Block struct { 24 | ID string `sql:",pk"` 25 | GuildID string `sql:",pk"` 26 | Type string 27 | } 28 | -------------------------------------------------------------------------------- /bot/util/constants.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/bwmarrin/discordgo" 8 | ) 9 | 10 | // Languages maps ISO codes to their native names 11 | var Languages = map[string]string{ 12 | "en-US": "American English", 13 | "nl-NL": "Nederlands", 14 | "de-DE": "Deutsch", 15 | } 16 | 17 | // LanguagesReversed is Languages with its keys and values flipped 18 | var LanguagesReversed = make(map[string]string) 19 | 20 | func init() { 21 | for k, v := range Languages { 22 | LanguagesReversed[strings.ToLower(v)] = k 23 | } 24 | } 25 | 26 | var reCustomEmoji = regexp.MustCompile(`<(a)?:(\w{2,32}):(\d{17,19})>`) 27 | 28 | // Permissions represents Discord permissions mapped by their bit value to their identifier 29 | var Permissions = map[int]string{ 30 | discordgo.PermissionCreateInstantInvite: "CREATE_INSTANT_INVITE", 31 | discordgo.PermissionKickMembers: "KICK_MEMBERS", 32 | discordgo.PermissionBanMembers: "BAN_MEMBERS", 33 | discordgo.PermissionAdministrator: "ADMINISTRATOR", 34 | discordgo.PermissionManageChannels: "MANAGE_CHANNELS", 35 | discordgo.PermissionManageServer: "MANAGE_GUILD", // Should be discordgo.PermissionManageGuild 36 | discordgo.PermissionAddReactions: "ADD_REACTIONS", 37 | discordgo.PermissionViewAuditLogs: "VIEW_AUDIT_LOG", 38 | discordgo.PermissionReadMessages: "VIEW_CHANNEL", // Should be discordgo.PermissionViewChannel 39 | discordgo.PermissionSendMessages: "SEND_MESSAGES", 40 | discordgo.PermissionSendTTSMessages: "SEND_TTS_MESSAGES", 41 | discordgo.PermissionManageMessages: "MANAGE_MESSAGES", 42 | discordgo.PermissionEmbedLinks: "EMBED_LINKS", 43 | discordgo.PermissionAttachFiles: "ATTACH_FILES", 44 | discordgo.PermissionReadMessageHistory: "READ_MESSAGE_HISTORY", 45 | discordgo.PermissionMentionEveryone: "MENTION_EVERYONE", 46 | discordgo.PermissionUseExternalEmojis: "USE_EXTERNAL_EMOJIS", 47 | discordgo.PermissionVoiceConnect: "CONNECT", 48 | discordgo.PermissionVoiceSpeak: "SPEAK", 49 | discordgo.PermissionVoiceMuteMembers: "MUTE_MEMBERS", 50 | discordgo.PermissionVoiceDeafenMembers: "DEAFEN_MEMBERS", 51 | discordgo.PermissionVoiceMoveMembers: "MOVE_MEMBERS", 52 | discordgo.PermissionVoiceUseVAD: "USE_VAD", 53 | 0x00000100: "PRIORITY_SPEAKER", // discordgo.PermissionPrioritySpeaker doesn't exist 54 | discordgo.PermissionChangeNickname: "CHANGE_NICKNAME", 55 | discordgo.PermissionManageNicknames: "MANAGE_NICKNAMES", 56 | discordgo.PermissionManageRoles: "MANAGE_ROLES", 57 | discordgo.PermissionManageWebhooks: "MANAGE_WEBHOOKS", 58 | discordgo.PermissionManageEmojis: "MANAGE_EMOJIS", 59 | } 60 | 61 | var emojis = map[string]string{ 62 | "😀": "grinning", 63 | "😬": "grimacing", 64 | "😁": "grin", 65 | "😂": "joy", 66 | "😃": "smiley", 67 | "😄": "smile", 68 | "😅": "sweat_smile", 69 | "😆": "laughing", 70 | "😇": "innocent", 71 | "😉": "wink", 72 | "😊": "blush", 73 | "🙂": "slight_smile", 74 | "🙃": "upside_down", 75 | "☺": "relaxed", 76 | "😋": "yum", 77 | "😌": "relieved", 78 | "😍": "heart_eyes", 79 | "😘": "kissing_heart", 80 | "😗": "kissing", 81 | "😙": "kissing_smiling_eyes", 82 | "😚": "kissing_closed_eyes", 83 | "😜": "stuck_out_tongue_winking_eye", 84 | "😝": "stuck_out_tongue_closed_eyes", 85 | "😛": "stuck_out_tongue", 86 | "🤑": "money_mouth", 87 | "🤓": "nerd", 88 | "😎": "sunglasses", 89 | "🤗": "hugging", 90 | "😏": "smirk", 91 | "😶": "no_mouth", 92 | "😐": "neutral_face", 93 | "😑": "expressionless", 94 | "😒": "unamused", 95 | "🙄": "rolling_eyes", 96 | "🤔": "thinking", 97 | "😳": "flushed", 98 | "😞": "disappointed", 99 | "😟": "worried", 100 | "😠": "angry", 101 | "😡": "rage", 102 | "😔": "pensive", 103 | "😕": "confused", 104 | "🙁": "slight_frown", 105 | "☹": "frowning2", 106 | "😣": "persevere", 107 | "😖": "confounded", 108 | "😫": "tired_face", 109 | "😩": "weary", 110 | "😤": "triumph", 111 | "😮": "open_mouth", 112 | "😱": "scream", 113 | "😨": "fearful", 114 | "😰": "cold_sweat", 115 | "😯": "hushed", 116 | "😦": "frowning", 117 | "😧": "anguished", 118 | "😢": "cry", 119 | "😥": "disappointed_relieved", 120 | "😪": "sleepy", 121 | "😓": "sweat", 122 | "😭": "sob", 123 | "😵": "dizzy_face", 124 | "😲": "astonished", 125 | "🤐": "zipper_mouth", 126 | "😷": "mask", 127 | "🤒": "thermometer_face", 128 | "🤕": "head_bandage", 129 | "😴": "sleeping", 130 | "💤": "zzz", 131 | "💩": "poop", 132 | "😈": "smiling_imp", 133 | "👿": "imp", 134 | "👹": "japanese_ogre", 135 | "👺": "japanese_goblin", 136 | "💀": "skull", 137 | "👻": "ghost", 138 | "👽": "alien", 139 | "🤖": "robot", 140 | "😺": "smiley_cat", 141 | "😸": "smile_cat", 142 | "😹": "joy_cat", 143 | "😻": "heart_eyes_cat", 144 | "😼": "smirk_cat", 145 | "😽": "kissing_cat", 146 | "🙀": "scream_cat", 147 | "😿": "crying_cat_face", 148 | "😾": "pouting_cat", 149 | "🙌": "raised_hands", 150 | "👏": "clap", 151 | "👋": "wave", 152 | "👍": "thumbsup", 153 | "👎": "thumbsdown", 154 | "👊": "punch", 155 | "✊": "fist", 156 | "✌": "v", 157 | "👌": "ok_hand", 158 | "✋": "raised_hand", 159 | "👐": "open_hands", 160 | "💪": "muscle", 161 | "🙏": "pray", 162 | "☝": "point_up", 163 | "👆": "point_up_2", 164 | "👇": "point_down", 165 | "👈": "point_left", 166 | "👉": "point_right", 167 | "🖕": "middle_finger", 168 | "🖐": "hand_splayed", 169 | "🤘": "metal", 170 | "🖖": "vulcan", 171 | "✍": "writing_hand", 172 | "💅": "nail_care", 173 | "👄": "lips", 174 | "👅": "tongue", 175 | "👂": "ear", 176 | "👃": "nose", 177 | "👁": "eye", 178 | "👀": "eyes", 179 | "👤": "bust_in_silhouette", 180 | "👥": "busts_in_silhouette", 181 | "🗣": "speaking_head", 182 | "👶": "baby", 183 | "👦": "boy", 184 | "👧": "girl", 185 | "👨": "man", 186 | "👩": "woman", 187 | "👱": "person_with_blond_hair", 188 | "👴": "older_man", 189 | "👵": "older_woman", 190 | "👲": "man_with_gua_pi_mao", 191 | "👳": "man_with_turban", 192 | "👮": "cop", 193 | "👷": "construction_worker", 194 | "💂": "guardsman", 195 | "🕵": "spy", 196 | "🎅": "santa", 197 | "👼": "angel", 198 | "👸": "princess", 199 | "👰": "bride_with_veil", 200 | "🚶": "walking", 201 | "🏃": "runner", 202 | "💃": "dancer", 203 | "👯": "dancers", 204 | "👫": "couple", 205 | "👬": "two_men_holding_hands", 206 | "👭": "two_women_holding_hands", 207 | "🙇": "bow", 208 | "💁": "information_desk_person", 209 | "🙅": "no_good", 210 | "🙆": "ok_woman", 211 | "🙋": "raising_hand", 212 | "🙎": "person_with_pouting_face", 213 | "🙍": "person_frowning", 214 | "💇": "haircut", 215 | "💆": "massage", 216 | "💑": "couple_with_heart", 217 | "👩‍❤️‍👩": "couple_ww", 218 | "👨‍❤️‍👨": "couple_mm", 219 | "💏": "couplekiss", 220 | "👩‍❤️‍💋‍👩": "kiss_ww", 221 | "👨‍❤️‍💋‍👨": "kiss_mm", 222 | "👪": "family", 223 | "👨‍👩‍👧": "family_mwg", 224 | "👨‍👩‍👧‍👦": "family_mwgb", 225 | "👨‍👩‍👦‍👦": "family_mwbb", 226 | "👨‍👩‍👧‍👧": "family_mwgg", 227 | "👩‍👩‍👦": "family_wwb", 228 | "👩‍👩‍👧": "family_wwg", 229 | "👩‍👩‍👧‍👦": "family_wwgb", 230 | "👩‍👩‍👦‍👦": "family_wwbb", 231 | "👩‍👩‍👧‍👧": "family_wwgg", 232 | "👨‍👨‍👦": "family_mmb", 233 | "👨‍👨‍👧": "family_mmg", 234 | "👨‍👨‍👧‍👦": "family_mmgb", 235 | "👨‍👨‍👦‍👦": "family_mmbb", 236 | "👨‍👨‍👧‍👧": "family_mmgg", 237 | "👚": "womans_clothes", 238 | "👕": "shirt", 239 | "👖": "jeans", 240 | "👔": "necktie", 241 | "👗": "dress", 242 | "👙": "bikini", 243 | "👘": "kimono", 244 | "💄": "lipstick", 245 | "💋": "kiss", 246 | "👣": "footprints", 247 | "👠": "high_heel", 248 | "👡": "sandal", 249 | "👢": "boot", 250 | "👞": "mans_shoe", 251 | "👟": "athletic_shoe", 252 | "👒": "womans_hat", 253 | "🎩": "tophat", 254 | "⛑": "helmet_with_cross", 255 | "🎓": "mortar_board", 256 | "👑": "crown", 257 | "🎒": "school_satchel", 258 | "👝": "pouch", 259 | "👛": "purse", 260 | "👜": "handbag", 261 | "💼": "briefcase", 262 | "👓": "eyeglasses", 263 | "🕶": "dark_sunglasses", 264 | "💍": "ring", 265 | "🌂": "closed_umbrella", 266 | "🤠": "cowboy", 267 | "🤡": "clown", 268 | "🤢": "nauseated_face", 269 | "🤣": "rofl", 270 | "🤤": "drooling_face", 271 | "🤥": "lying_face", 272 | "🤧": "sneezing_face", 273 | "🤴": "prince", 274 | "🤵": "man_in_tuxedo", 275 | "🤶": "mrs_claus", 276 | "🤦": "face_palm", 277 | "🤷": "shrug", 278 | "🤰": "pregnant_woman", 279 | "🤳": "selfie", 280 | "🕺": "man_dancing", 281 | "🤙": "call_me", 282 | "🤚": "raised_back_of_hand", 283 | "🤛": "left_facing_fist", 284 | "🤜": "right_facing_fist", 285 | "🤝": "handshake", 286 | "🤞": "fingers_crossed", 287 | "🐶": "dog", 288 | "🐱": "cat", 289 | "🐭": "mouse", 290 | "🐹": "hamster", 291 | "🐰": "rabbit", 292 | "🐻": "bear", 293 | "🐼": "panda_face", 294 | "🐨": "koala", 295 | "🐯": "tiger", 296 | "🦁": "lion_face", 297 | "🐮": "cow", 298 | "🐷": "pig", 299 | "🐽": "pig_nose", 300 | "🐸": "frog", 301 | "🐙": "octopus", 302 | "🐵": "monkey_face", 303 | "🙈": "see_no_evil", 304 | "🙉": "hear_no_evil", 305 | "🙊": "speak_no_evil", 306 | "🐒": "monkey", 307 | "🐔": "chicken", 308 | "🐧": "penguin", 309 | "🐦": "bird", 310 | "🐤": "baby_chick", 311 | "🐣": "hatching_chick", 312 | "🐥": "hatched_chick", 313 | "🐺": "wolf", 314 | "🐗": "boar", 315 | "🐴": "horse", 316 | "🦄": "unicorn", 317 | "🐝": "bee", 318 | "🐛": "bug", 319 | "🐌": "snail", 320 | "🐞": "beetle", 321 | "🐜": "ant", 322 | "🕷": "spider", 323 | "🦂": "scorpion", 324 | "🦀": "crab", 325 | "🐍": "snake", 326 | "🐢": "turtle", 327 | "🐠": "tropical_fish", 328 | "🐟": "fish", 329 | "🐡": "blowfish", 330 | "🐬": "dolphin", 331 | "🐳": "whale", 332 | "🐋": "whale2", 333 | "🐊": "crocodile", 334 | "🐆": "leopard", 335 | "🐅": "tiger2", 336 | "🐃": "water_buffalo", 337 | "🐂": "ox", 338 | "🐄": "cow2", 339 | "🐪": "dromedary_camel", 340 | "🐫": "camel", 341 | "🐘": "elephant", 342 | "🐐": "goat", 343 | "🐏": "ram", 344 | "🐑": "sheep", 345 | "🐎": "racehorse", 346 | "🐖": "pig2", 347 | "🐀": "rat", 348 | "🐁": "mouse2", 349 | "🐓": "rooster", 350 | "🦃": "turkey", 351 | "🕊": "dove", 352 | "🐕": "dog2", 353 | "🐩": "poodle", 354 | "🐈": "cat2", 355 | "🐇": "rabbit2", 356 | "🐿": "chipmunk", 357 | "🐾": "feet", 358 | "🐉": "dragon", 359 | "🐲": "dragon_face", 360 | "🌵": "cactus", 361 | "🎄": "christmas_tree", 362 | "🌲": "evergreen_tree", 363 | "🌳": "deciduous_tree", 364 | "🌴": "palm_tree", 365 | "🌱": "seedling", 366 | "🌿": "herb", 367 | "☘": "shamrock", 368 | "🍀": "four_leaf_clover", 369 | "🎍": "bamboo", 370 | "🎋": "tanabata_tree", 371 | "🍃": "leaves", 372 | "🍂": "fallen_leaf", 373 | "🍁": "maple_leaf", 374 | "🌾": "ear_of_rice", 375 | "🌺": "hibiscus", 376 | "🌻": "sunflower", 377 | "🌹": "rose", 378 | "🌷": "tulip", 379 | "🌼": "blossom", 380 | "🌸": "cherry_blossom", 381 | "💐": "bouquet", 382 | "🍄": "mushroom", 383 | "🌰": "chestnut", 384 | "🎃": "jack_o_lantern", 385 | "🐚": "shell", 386 | "🕸": "spider_web", 387 | "🌎": "earth_americas", 388 | "🌍": "earth_africa", 389 | "🌏": "earth_asia", 390 | "🌕": "full_moon", 391 | "🌖": "waning_gibbous_moon", 392 | "🌗": "last_quarter_moon", 393 | "🌘": "waning_crescent_moon", 394 | "🌑": "new_moon", 395 | "🌒": "waxing_crescent_moon", 396 | "🌓": "first_quarter_moon", 397 | "🌔": "waxing_gibbous_moon", 398 | "🌚": "new_moon_with_face", 399 | "🌝": "full_moon_with_face", 400 | "🌛": "first_quarter_moon_with_face", 401 | "🌜": "last_quarter_moon_with_face", 402 | "🌞": "sun_with_face", 403 | "🌙": "crescent_moon", 404 | "⭐": "star", 405 | "🌟": "star2", 406 | "💫": "dizzy", 407 | "✨": "sparkles", 408 | "☄": "comet", 409 | "☀": "sunny", 410 | "🌤": "white_sun_small_cloud", 411 | "⛅": "partly_sunny", 412 | "🌥": "white_sun_cloud", 413 | "🌦": "white_sun_rain_cloud", 414 | "☁": "cloud", 415 | "🌧": "cloud_rain", 416 | "⛈": "thunder_cloud_rain", 417 | "🌩": "cloud_lightning", 418 | "⚡": "zap", 419 | "🔥": "fire", 420 | "💥": "boom", 421 | "❄": "snowflake", 422 | "🌨": "cloud_snow", 423 | "☃": "snowman2", 424 | "⛄": "snowman", 425 | "🌬": "wind_blowing_face", 426 | "💨": "dash", 427 | "🌪": "cloud_tornado", 428 | "🌫": "fog", 429 | "☂": "umbrella2", 430 | "☔": "umbrella", 431 | "💧": "droplet", 432 | "💦": "sweat_drops", 433 | "🌊": "ocean", 434 | "🦅": "eagle", 435 | "🦆": "duck", 436 | "🦇": "bat", 437 | "🦈": "shark", 438 | "🦉": "owl", 439 | "🦊": "fox", 440 | "🦋": "butterfly", 441 | "🦌": "deer", 442 | "🦍": "gorilla", 443 | "🦎": "lizard", 444 | "🦏": "rhino", 445 | "🥀": "wilted_rose", 446 | "🦐": "shrimp", 447 | "🦑": "squid", 448 | "🍏": "green_apple", 449 | "🍎": "apple", 450 | "🍐": "pear", 451 | "🍊": "tangerine", 452 | "🍋": "lemon", 453 | "🍌": "banana", 454 | "🍉": "watermelon", 455 | "🍇": "grapes", 456 | "🍓": "strawberry", 457 | "🍈": "melon", 458 | "🍒": "cherries", 459 | "🍑": "peach", 460 | "🍍": "pineapple", 461 | "🍅": "tomato", 462 | "🍆": "eggplant", 463 | "🌶": "hot_pepper", 464 | "🌽": "corn", 465 | "🍠": "sweet_potato", 466 | "🍯": "honey_pot", 467 | "🍞": "bread", 468 | "🧀": "cheese", 469 | "🍗": "poultry_leg", 470 | "🍖": "meat_on_bone", 471 | "🍤": "fried_shrimp", 472 | "🍳": "cooking", 473 | "🍔": "hamburger", 474 | "🍟": "fries", 475 | "🌭": "hotdog", 476 | "🍕": "pizza", 477 | "🍝": "spaghetti", 478 | "🌮": "taco", 479 | "🌯": "burrito", 480 | "🍜": "ramen", 481 | "🍲": "stew", 482 | "🍥": "fish_cake", 483 | "🍣": "sushi", 484 | "🍱": "bento", 485 | "🍛": "curry", 486 | "🍙": "rice_ball", 487 | "🍚": "rice", 488 | "🍘": "rice_cracker", 489 | "🍢": "oden", 490 | "🍡": "dango", 491 | "🍧": "shaved_ice", 492 | "🍨": "ice_cream", 493 | "🍦": "icecream", 494 | "🍰": "cake", 495 | "🎂": "birthday", 496 | "🍮": "custard", 497 | "🍬": "candy", 498 | "🍭": "lollipop", 499 | "🍫": "chocolate_bar", 500 | "🍿": "popcorn", 501 | "🍩": "doughnut", 502 | "🍪": "cookie", 503 | "🍺": "beer", 504 | "🍻": "beers", 505 | "🍷": "wine_glass", 506 | "🍸": "cocktail", 507 | "🍹": "tropical_drink", 508 | "🍾": "champagne", 509 | "🍶": "sake", 510 | "🍵": "tea", 511 | "☕": "coffee", 512 | "🍼": "baby_bottle", 513 | "🍴": "fork_and_knife", 514 | "🍽": "fork_knife_plate", 515 | "🥐": "croissant", 516 | "🥑": "avocado", 517 | "🥒": "cucumber", 518 | "🥓": "bacon", 519 | "🥔": "potato", 520 | "🥕": "carrot", 521 | "🥖": "french_bread", 522 | "🥗": "salad", 523 | "🥘": "shallow_pan_of_food", 524 | "🥙": "stuffed_flatbread", 525 | "🥂": "champagne_glass", 526 | "🥃": "tumbler_glass", 527 | "🥄": "spoon", 528 | "🥚": "egg", 529 | "🥛": "milk", 530 | "🥜": "peanuts", 531 | "🥝": "kiwi", 532 | "🥞": "pancakes", 533 | "⚽": "soccer", 534 | "🏀": "basketball", 535 | "🏈": "football", 536 | "⚾": "baseball", 537 | "🎾": "tennis", 538 | "🏐": "volleyball", 539 | "🏉": "rugby_football", 540 | "🎱": "8ball", 541 | "⛳": "golf", 542 | "🏌": "golfer", 543 | "🏓": "ping_pong", 544 | "🏸": "badminton", 545 | "🏒": "hockey", 546 | "🏑": "field_hockey", 547 | "🏏": "cricket", 548 | "🎿": "ski", 549 | "⛷": "skier", 550 | "🏂": "snowboarder", 551 | "⛸": "ice_skate", 552 | "🏹": "bow_and_arrow", 553 | "🎣": "fishing_pole_and_fish", 554 | "🚣": "rowboat", 555 | "🏊": "swimmer", 556 | "🏄": "surfer", 557 | "🛀": "bath", 558 | "⛹": "basketball_player", 559 | "🏋": "lifter", 560 | "🚴": "bicyclist", 561 | "🚵": "mountain_bicyclist", 562 | "🏇": "horse_racing", 563 | "🕴": "levitate", 564 | "🏆": "trophy", 565 | "🎽": "running_shirt_with_sash", 566 | "🏅": "medal", 567 | "🎖": "military_medal", 568 | "🎗": "reminder_ribbon", 569 | "🏵": "rosette", 570 | "🎫": "ticket", 571 | "🎟": "tickets", 572 | "🎭": "performing_arts", 573 | "🎨": "art", 574 | "🎪": "circus_tent", 575 | "🎤": "microphone", 576 | "🎧": "headphones", 577 | "🎼": "musical_score", 578 | "🎹": "musical_keyboard", 579 | "🎷": "saxophone", 580 | "🎺": "trumpet", 581 | "🎸": "guitar", 582 | "🎻": "violin", 583 | "🎬": "clapper", 584 | "🎮": "video_game", 585 | "👾": "space_invader", 586 | "🎯": "dart", 587 | "🎲": "game_die", 588 | "🎰": "slot_machine", 589 | "🎳": "bowling", 590 | "🤸": "cartwheel", 591 | "🤹": "juggling", 592 | "🤼": "wrestlers", 593 | "🥊": "boxing_glove", 594 | "🥋": "martial_arts_uniform", 595 | "🤽": "water_polo", 596 | "🤾": "handball", 597 | "🥅": "goal", 598 | "🤺": "fencer", 599 | "🥇": "first_place", 600 | "🥈": "second_place", 601 | "🥉": "third_place", 602 | "🥁": "drum", 603 | "🚗": "red_car", 604 | "🚕": "taxi", 605 | "🚙": "blue_car", 606 | "🚌": "bus", 607 | "🚎": "trolleybus", 608 | "🏎": "race_car", 609 | "🚓": "police_car", 610 | "🚑": "ambulance", 611 | "🚒": "fire_engine", 612 | "🚐": "minibus", 613 | "🚚": "truck", 614 | "🚛": "articulated_lorry", 615 | "🚜": "tractor", 616 | "🏍": "motorcycle", 617 | "🚲": "bike", 618 | "🚨": "rotating_light", 619 | "🚔": "oncoming_police_car", 620 | "🚍": "oncoming_bus", 621 | "🚘": "oncoming_automobile", 622 | "🚖": "oncoming_taxi", 623 | "🚡": "aerial_tramway", 624 | "🚠": "mountain_cableway", 625 | "🚟": "suspension_railway", 626 | "🚃": "railway_car", 627 | "🚋": "train", 628 | "🚝": "monorail", 629 | "🚄": "bullettrain_side", 630 | "🚅": "bullettrain_front", 631 | "🚈": "light_rail", 632 | "🚞": "mountain_railway", 633 | "🚂": "steam_locomotive", 634 | "🚆": "train2", 635 | "🚇": "metro", 636 | "🚊": "tram", 637 | "🚉": "station", 638 | "🚁": "helicopter", 639 | "🛩": "airplane_small", 640 | "✈": "airplane", 641 | "🛫": "airplane_departure", 642 | "🛬": "airplane_arriving", 643 | "⛵": "sailboat", 644 | "🛥": "motorboat", 645 | "🚤": "speedboat", 646 | "⛴": "ferry", 647 | "🛳": "cruise_ship", 648 | "🚀": "rocket", 649 | "🛰": "satellite_orbital", 650 | "💺": "seat", 651 | "⚓": "anchor", 652 | "🚧": "construction", 653 | "⛽": "fuelpump", 654 | "🚏": "busstop", 655 | "🚦": "vertical_traffic_light", 656 | "🚥": "traffic_light", 657 | "🏁": "checkered_flag", 658 | "🚢": "ship", 659 | "🎡": "ferris_wheel", 660 | "🎢": "roller_coaster", 661 | "🎠": "carousel_horse", 662 | "🏗": "construction_site", 663 | "🌁": "foggy", 664 | "🗼": "tokyo_tower", 665 | "🏭": "factory", 666 | "⛲": "fountain", 667 | "🎑": "rice_scene", 668 | "⛰": "mountain", 669 | "🏔": "mountain_snow", 670 | "🗻": "mount_fuji", 671 | "🌋": "volcano", 672 | "🗾": "japan", 673 | "🏕": "camping", 674 | "⛺": "tent", 675 | "🏞": "park", 676 | "🛣": "motorway", 677 | "🛤": "railway_track", 678 | "🌅": "sunrise", 679 | "🌄": "sunrise_over_mountains", 680 | "🏜": "desert", 681 | "🏖": "beach", 682 | "🏝": "island", 683 | "🌇": "city_sunset", 684 | "🌆": "city_dusk", 685 | "🏙": "cityscape", 686 | "🌃": "night_with_stars", 687 | "🌉": "bridge_at_night", 688 | "🌌": "milky_way", 689 | "🌠": "stars", 690 | "🎇": "sparkler", 691 | "🎆": "fireworks", 692 | "🌈": "rainbow", 693 | "🏘": "homes", 694 | "🏰": "european_castle", 695 | "🏯": "japanese_castle", 696 | "🏟": "stadium", 697 | "🗽": "statue_of_liberty", 698 | "🏠": "house", 699 | "🏡": "house_with_garden", 700 | "🏚": "house_abandoned", 701 | "🏢": "office", 702 | "🏬": "department_store", 703 | "🏣": "post_office", 704 | "🏤": "european_post_office", 705 | "🏥": "hospital", 706 | "🏦": "bank", 707 | "🏨": "hotel", 708 | "🏪": "convenience_store", 709 | "🏫": "school", 710 | "🏩": "love_hotel", 711 | "💒": "wedding", 712 | "🏛": "classical_building", 713 | "⛪": "church", 714 | "🕌": "mosque", 715 | "🕍": "synagogue", 716 | "🕋": "kaaba", 717 | "⛩": "shinto_shrine", 718 | "🛴": "scooter", 719 | "🛵": "motor_scooter", 720 | "🛶": "canoe", 721 | "⌚": "watch", 722 | "📱": "iphone", 723 | "📲": "calling", 724 | "💻": "computer", 725 | "⌨": "keyboard", 726 | "🖥": "desktop", 727 | "🖨": "printer", 728 | "🖱": "mouse_three_button", 729 | "🖲": "trackball", 730 | "🕹": "joystick", 731 | "🗜": "compression", 732 | "💽": "minidisc", 733 | "💾": "floppy_disk", 734 | "💿": "cd", 735 | "📀": "dvd", 736 | "📼": "vhs", 737 | "📷": "camera", 738 | "📸": "camera_with_flash", 739 | "📹": "video_camera", 740 | "🎥": "movie_camera", 741 | "📽": "projector", 742 | "🎞": "film_frames", 743 | "📞": "telephone_receiver", 744 | "☎": "telephone", 745 | "📟": "pager", 746 | "📠": "fax", 747 | "📺": "tv", 748 | "📻": "radio", 749 | "🎙": "microphone2", 750 | "🎚": "level_slider", 751 | "🎛": "control_knobs", 752 | "⏱": "stopwatch", 753 | "⏲": "timer", 754 | "⏰": "alarm_clock", 755 | "🕰": "clock", 756 | "⏳": "hourglass_flowing_sand", 757 | "⌛": "hourglass", 758 | "📡": "satellite", 759 | "🔋": "battery", 760 | "🔌": "electric_plug", 761 | "💡": "bulb", 762 | "🔦": "flashlight", 763 | "🕯": "candle", 764 | "🗑": "wastebasket", 765 | "🛢": "oil", 766 | "💸": "money_with_wings", 767 | "💵": "dollar", 768 | "💴": "yen", 769 | "💶": "euro", 770 | "💷": "pound", 771 | "💰": "moneybag", 772 | "💳": "credit_card", 773 | "💎": "gem", 774 | "⚖": "scales", 775 | "🔧": "wrench", 776 | "🔨": "hammer", 777 | "⚒": "hammer_pick", 778 | "🛠": "tools", 779 | "⛏": "pick", 780 | "🔩": "nut_and_bolt", 781 | "⚙": "gear", 782 | "⛓": "chains", 783 | "🔫": "gun", 784 | "💣": "bomb", 785 | "🔪": "knife", 786 | "🗡": "dagger", 787 | "⚔": "crossed_swords", 788 | "🛡": "shield", 789 | "🚬": "smoking", 790 | "☠": "skull_crossbones", 791 | "⚰": "coffin", 792 | "⚱": "urn", 793 | "🏺": "amphora", 794 | "🔮": "crystal_ball", 795 | "📿": "prayer_beads", 796 | "💈": "barber", 797 | "⚗": "alembic", 798 | "🔭": "telescope", 799 | "🔬": "microscope", 800 | "🕳": "hole", 801 | "💊": "pill", 802 | "💉": "syringe", 803 | "🌡": "thermometer", 804 | "🏷": "label", 805 | "🔖": "bookmark", 806 | "🚽": "toilet", 807 | "🚿": "shower", 808 | "🛁": "bathtub", 809 | "🔑": "key", 810 | "🗝": "key2", 811 | "🛋": "couch", 812 | "🛌": "sleeping_accommodation", 813 | "🛏": "bed", 814 | "🚪": "door", 815 | "🛎": "bellhop", 816 | "🖼": "frame_photo", 817 | "🗺": "map", 818 | "⛱": "beach_umbrella", 819 | "🗿": "moyai", 820 | "🛍": "shopping_bags", 821 | "🎈": "balloon", 822 | "🎏": "flags", 823 | "🎀": "ribbon", 824 | "🎁": "gift", 825 | "🎊": "confetti_ball", 826 | "🎉": "tada", 827 | "🎎": "dolls", 828 | "🎐": "wind_chime", 829 | "🎌": "crossed_flags", 830 | "🏮": "izakaya_lantern", 831 | "✉": "envelope", 832 | "📩": "envelope_with_arrow", 833 | "📨": "incoming_envelope", 834 | "📧": "e_mail", 835 | "💌": "love_letter", 836 | "📮": "postbox", 837 | "📪": "mailbox_closed", 838 | "📫": "mailbox", 839 | "📬": "mailbox_with_mail", 840 | "📭": "mailbox_with_no_mail", 841 | "📦": "package", 842 | "📯": "postal_horn", 843 | "📥": "inbox_tray", 844 | "📤": "outbox_tray", 845 | "📜": "scroll", 846 | "📃": "page_with_curl", 847 | "📑": "bookmark_tabs", 848 | "📊": "bar_chart", 849 | "📈": "chart_with_upwards_trend", 850 | "📉": "chart_with_downwards_trend", 851 | "📄": "page_facing_up", 852 | "📅": "date", 853 | "📆": "calendar", 854 | "🗓": "calendar_spiral", 855 | "📇": "card_index", 856 | "🗃": "card_box", 857 | "🗳": "ballot_box", 858 | "🗄": "file_cabinet", 859 | "📋": "clipboard", 860 | "🗒": "notepad_spiral", 861 | "📁": "file_folder", 862 | "📂": "open_file_folder", 863 | "🗂": "dividers", 864 | "🗞": "newspaper2", 865 | "📰": "newspaper", 866 | "📓": "notebook", 867 | "📕": "closed_book", 868 | "📗": "green_book", 869 | "📘": "blue_book", 870 | "📙": "orange_book", 871 | "📔": "notebook_with_decorative_cover", 872 | "📒": "ledger", 873 | "📚": "books", 874 | "📖": "book", 875 | "🔗": "link", 876 | "📎": "paperclip", 877 | "🖇": "paperclips", 878 | "✂": "scissors", 879 | "📐": "triangular_ruler", 880 | "📏": "straight_ruler", 881 | "📌": "pushpin", 882 | "📍": "round_pushpin", 883 | "🚩": "triangular_flag_on_post", 884 | "🏳": "flag_white", 885 | "🏴": "flag_black", 886 | "🔐": "closed_lock_with_key", 887 | "🔒": "lock", 888 | "🔓": "unlock", 889 | "🔏": "lock_with_ink_pen", 890 | "🖊": "pen_ballpoint", 891 | "🖋": "pen_fountain", 892 | "✒": "black_nib", 893 | "📝": "pencil", 894 | "✏": "pencil2", 895 | "🖍": "crayon", 896 | "🖌": "paintbrush", 897 | "🔍": "mag", 898 | "🔎": "mag_right", 899 | "🛒": "shopping_cart", 900 | "💯": "100", 901 | "🔢": "1234", 902 | "❤": "heart", 903 | "💛": "yellow_heart", 904 | "💚": "green_heart", 905 | "💙": "blue_heart", 906 | "💜": "purple_heart", 907 | "💔": "broken_heart", 908 | "❣": "heart_exclamation", 909 | "💕": "two_hearts", 910 | "💞": "revolving_hearts", 911 | "💓": "heartbeat", 912 | "💗": "heartpulse", 913 | "💖": "sparkling_heart", 914 | "💘": "cupid", 915 | "💝": "gift_heart", 916 | "💟": "heart_decoration", 917 | "☮": "peace", 918 | "✝": "cross", 919 | "☪": "star_and_crescent", 920 | "🕉": "om_symbol", 921 | "☸": "wheel_of_dharma", 922 | "✡": "star_of_david", 923 | "🔯": "six_pointed_star", 924 | "🕎": "menorah", 925 | "☯": "yin_yang", 926 | "☦": "orthodox_cross", 927 | "🛐": "place_of_worship", 928 | "⛎": "ophiuchus", 929 | "♈": "aries", 930 | "♉": "taurus", 931 | "♊": "gemini", 932 | "♋": "cancer", 933 | "♌": "leo", 934 | "♍": "virgo", 935 | "♎": "libra", 936 | "♏": "scorpius", 937 | "♐": "sagittarius", 938 | "♑": "capricorn", 939 | "♒": "aquarius", 940 | "♓": "pisces", 941 | "🆔": "id", 942 | "⚛": "atom", 943 | "🈳": "u7a7a", 944 | "🈹": "u5272", 945 | "☢": "radioactive", 946 | "☣": "biohazard", 947 | "📴": "mobile_phone_off", 948 | "📳": "vibration_mode", 949 | "🈶": "u6709", 950 | "🈚": "u7121", 951 | "🈸": "u7533", 952 | "🈺": "u55b6", 953 | "🈷": "u6708", 954 | "✴": "eight_pointed_black_star", 955 | "🆚": "vs", 956 | "🉑": "accept", 957 | "💮": "white_flower", 958 | "🉐": "ideograph_advantage", 959 | "㊙": "secret", 960 | "㊗": "congratulations", 961 | "🈴": "u5408", 962 | "🈵": "u6e80", 963 | "🈲": "u7981", 964 | "🅰": "a", 965 | "🅱": "b", 966 | "🆎": "ab", 967 | "🆑": "cl", 968 | "🅾": "o2", 969 | "🆘": "sos", 970 | "⛔": "no_entry", 971 | "📛": "name_badge", 972 | "🚫": "no_entry_sign", 973 | "❌": "x", 974 | "⭕": "o", 975 | "💢": "anger", 976 | "♨": "hotsprings", 977 | "🚷": "no_pedestrians", 978 | "🚯": "do_not_litter", 979 | "🚳": "no_bicycles", 980 | "🚱": "non_potable_water", 981 | "🔞": "underage", 982 | "📵": "no_mobile_phones", 983 | "❗": "exclamation", 984 | "❕": "grey_exclamation", 985 | "❓": "question", 986 | "❔": "grey_question", 987 | "‼": "bangbang", 988 | "⁉": "interrobang", 989 | "🔅": "low_brightness", 990 | "🔆": "high_brightness", 991 | "🔱": "trident", 992 | "⚜": "fleur_de_lis", 993 | "〽": "part_alternation_mark", 994 | "⚠": "warning", 995 | "🚸": "children_crossing", 996 | "🔰": "beginner", 997 | "♻": "recycle", 998 | "🈯": "u6307", 999 | "💹": "chart", 1000 | "❇": "sparkle", 1001 | "✳": "eight_spoked_asterisk", 1002 | "❎": "negative_squared_cross_mark", 1003 | "✅": "white_check_mark", 1004 | "💠": "diamond_shape_with_a_dot_inside", 1005 | "🌀": "cyclone", 1006 | "➿": "loop", 1007 | "🌐": "globe_with_meridians", 1008 | "Ⓜ": "m", 1009 | "🏧": "atm", 1010 | "🈂": "sa", 1011 | "🛂": "passport_control", 1012 | "🛃": "customs", 1013 | "🛄": "baggage_claim", 1014 | "🛅": "left_luggage", 1015 | "♿": "wheelchair", 1016 | "🚭": "no_smoking", 1017 | "🚾": "wc", 1018 | "🅿": "parking", 1019 | "🚰": "potable_water", 1020 | "🚹": "mens", 1021 | "🚺": "womens", 1022 | "🚼": "baby_symbol", 1023 | "🚻": "restroom", 1024 | "🚮": "put_litter_in_its_place", 1025 | "🎦": "cinema", 1026 | "📶": "signal_strength", 1027 | "🈁": "koko", 1028 | "🆖": "ng", 1029 | "🆗": "ok", 1030 | "🆙": "up", 1031 | "🆒": "cool", 1032 | "🆕": "new", 1033 | "🆓": "free", 1034 | "0⃣": "zero", 1035 | "1⃣": "one", 1036 | "2⃣": "two", 1037 | "3⃣": "three", 1038 | "4⃣": "four", 1039 | "5⃣": "five", 1040 | "6⃣": "six", 1041 | "7⃣": "seven", 1042 | "8⃣": "eight", 1043 | "9⃣": "nine", 1044 | "🔟": "keycap_ten", 1045 | "▶": "arrow_forward", 1046 | "⏸": "pause_button", 1047 | "⏯": "play_pause", 1048 | "⏹": "stop_button", 1049 | "⏺": "record_button", 1050 | "⏭": "track_next", 1051 | "⏮": "track_previous", 1052 | "⏩": "fast_forward", 1053 | "⏪": "rewind", 1054 | "🔀": "twisted_rightwards_arrows", 1055 | "🔁": "repeat", 1056 | "🔂": "repeat_one", 1057 | "◀": "arrow_backward", 1058 | "🔼": "arrow_up_small", 1059 | "🔽": "arrow_down_small", 1060 | "⏫": "arrow_double_up", 1061 | "⏬": "arrow_double_down", 1062 | "➡": "arrow_right", 1063 | "⬅": "arrow_left", 1064 | "⬆": "arrow_up", 1065 | "⬇": "arrow_down", 1066 | "↗": "arrow_upper_right", 1067 | "↘": "arrow_lower_right", 1068 | "↙": "arrow_lower_left", 1069 | "↖": "arrow_upper_left", 1070 | "↕": "arrow_up_down", 1071 | "↔": "left_right_arrow", 1072 | "🔄": "arrows_counterclockwise", 1073 | "↪": "arrow_right_hook", 1074 | "↩": "leftwards_arrow_with_hook", 1075 | "⤴": "arrow_heading_up", 1076 | "⤵": "arrow_heading_down", 1077 | "#⃣": "hash", 1078 | "*⃣": "asterisk", 1079 | "ℹ": "information_source", 1080 | "🔤": "abc", 1081 | "🔡": "abcd", 1082 | "🔠": "capital_abcd", 1083 | "🔣": "symbols", 1084 | "🎵": "musical_note", 1085 | "🎶": "notes", 1086 | "〰": "wavy_dash", 1087 | "➰": "curly_loop", 1088 | "✔": "heavy_check_mark", 1089 | "🔃": "arrows_clockwise", 1090 | "➕": "heavy_plus_sign", 1091 | "➖": "heavy_minus_sign", 1092 | "➗": "heavy_division_sign", 1093 | "✖": "heavy_multiplication_x", 1094 | "💲": "heavy_dollar_sign", 1095 | "💱": "currency_exchange", 1096 | "©": "copyright", 1097 | "®": "registered", 1098 | "™": "tm", 1099 | "🔚": "end", 1100 | "🔙": "back", 1101 | "🔛": "on", 1102 | "🔝": "top", 1103 | "🔜": "soon", 1104 | "☑": "ballot_box_with_check", 1105 | "🔘": "radio_button", 1106 | "⚪": "white_circle", 1107 | "⚫": "black_circle", 1108 | "🔴": "red_circle", 1109 | "🔵": "large_blue_circle", 1110 | "🔸": "small_orange_diamond", 1111 | "🔹": "small_blue_diamond", 1112 | "🔶": "large_orange_diamond", 1113 | "🔷": "large_blue_diamond", 1114 | "🔺": "small_red_triangle", 1115 | "▪": "black_small_square", 1116 | "▫": "white_small_square", 1117 | "⬛": "black_large_square", 1118 | "⬜": "white_large_square", 1119 | "🔻": "small_red_triangle_down", 1120 | "◼": "black_medium_square", 1121 | "◻": "white_medium_square", 1122 | "◾": "black_medium_small_square", 1123 | "◽": "white_medium_small_square", 1124 | "🔲": "black_square_button", 1125 | "🔳": "white_square_button", 1126 | "🔈": "speaker", 1127 | "🔉": "sound", 1128 | "🔊": "loud_sound", 1129 | "🔇": "mute", 1130 | "📣": "mega", 1131 | "📢": "loudspeaker", 1132 | "🔔": "bell", 1133 | "🔕": "no_bell", 1134 | "🃏": "black_joker", 1135 | "🀄": "mahjong", 1136 | "♠": "spades", 1137 | "♣": "clubs", 1138 | "♥": "hearts", 1139 | "♦": "diamonds", 1140 | "🎴": "flower_playing_cards", 1141 | "💭": "thought_balloon", 1142 | "🗯": "anger_right", 1143 | "💬": "speech_balloon", 1144 | "🕐": "clock1", 1145 | "🕑": "clock2", 1146 | "🕒": "clock3", 1147 | "🕓": "clock4", 1148 | "🕔": "clock5", 1149 | "🕕": "clock6", 1150 | "🕖": "clock7", 1151 | "🕗": "clock8", 1152 | "🕘": "clock9", 1153 | "🕙": "clock10", 1154 | "🕚": "clock11", 1155 | "🕛": "clock12", 1156 | "🕜": "clock130", 1157 | "🕝": "clock230", 1158 | "🕞": "clock330", 1159 | "🕟": "clock430", 1160 | "🕠": "clock530", 1161 | "🕡": "clock630", 1162 | "🕢": "clock730", 1163 | "🕣": "clock830", 1164 | "🕤": "clock930", 1165 | "🕥": "clock1030", 1166 | "🕦": "clock1130", 1167 | "🕧": "clock1230", 1168 | "👁‍🗨": "eye_in_speech_bubble", 1169 | "🗨": "speech_left", 1170 | "⏏": "eject", 1171 | "🖤": "black_heart", 1172 | "🛑": "octagonal_sign", 1173 | "🇿": "regional_indicator_z", 1174 | "🇾": "regional_indicator_y", 1175 | "🇽": "regional_indicator_x", 1176 | "🇼": "regional_indicator_w", 1177 | "🇻": "regional_indicator_v", 1178 | "🇺": "regional_indicator_u", 1179 | "🇹": "regional_indicator_t", 1180 | "🇸": "regional_indicator_s", 1181 | "🇷": "regional_indicator_r", 1182 | "🇶": "regional_indicator_q", 1183 | "🇵": "regional_indicator_p", 1184 | "🇴": "regional_indicator_o", 1185 | "🇳": "regional_indicator_n", 1186 | "🇲": "regional_indicator_m", 1187 | "🇱": "regional_indicator_l", 1188 | "🇰": "regional_indicator_k", 1189 | "🇯": "regional_indicator_j", 1190 | "🇮": "regional_indicator_i", 1191 | "🇭": "regional_indicator_h", 1192 | "🇬": "regional_indicator_g", 1193 | "🇫": "regional_indicator_f", 1194 | "🇪": "regional_indicator_e", 1195 | "🇩": "regional_indicator_d", 1196 | "🇨": "regional_indicator_c", 1197 | "🇧": "regional_indicator_b", 1198 | "🇦": "regional_indicator_a", 1199 | "🇦🇨": "flag_ac", 1200 | "🇦🇫": "flag_af", 1201 | "🇦🇱": "flag_al", 1202 | "🇩🇿": "flag_dz", 1203 | "🇦🇩": "flag_ad", 1204 | "🇦🇴": "flag_ao", 1205 | "🇦🇮": "flag_ai", 1206 | "🇦🇬": "flag_ag", 1207 | "🇦🇷": "flag_ar", 1208 | "🇦🇲": "flag_am", 1209 | "🇦🇼": "flag_aw", 1210 | "🇦🇺": "flag_au", 1211 | "🇦🇹": "flag_at", 1212 | "🇦🇿": "flag_az", 1213 | "🇧🇸": "flag_bs", 1214 | "🇧🇭": "flag_bh", 1215 | "🇧🇩": "flag_bd", 1216 | "🇧🇧": "flag_bb", 1217 | "🇧🇾": "flag_by", 1218 | "🇧🇪": "flag_be", 1219 | "🇧🇿": "flag_bz", 1220 | "🇧🇯": "flag_bj", 1221 | "🇧🇲": "flag_bm", 1222 | "🇧🇹": "flag_bt", 1223 | "🇧🇴": "flag_bo", 1224 | "🇧🇦": "flag_ba", 1225 | "🇧🇼": "flag_bw", 1226 | "🇧🇷": "flag_br", 1227 | "🇧🇳": "flag_bn", 1228 | "🇧🇬": "flag_bg", 1229 | "🇧🇫": "flag_bf", 1230 | "🇧🇮": "flag_bi", 1231 | "🇨🇻": "flag_cv", 1232 | "🇰🇭": "flag_kh", 1233 | "🇨🇲": "flag_cm", 1234 | "🇨🇦": "flag_ca", 1235 | "🇰🇾": "flag_ky", 1236 | "🇨🇫": "flag_cf", 1237 | "🇹🇩": "flag_td", 1238 | "🇨🇱": "flag_cl", 1239 | "🇨🇳": "flag_cn", 1240 | "🇨🇴": "flag_co", 1241 | "🇰🇲": "flag_km", 1242 | "🇨🇬": "flag_cg", 1243 | "🇨🇩": "flag_cd", 1244 | "🇨🇷": "flag_cr", 1245 | "🇭🇷": "flag_hr", 1246 | "🇨🇺": "flag_cu", 1247 | "🇨🇾": "flag_cy", 1248 | "🇨🇿": "flag_cz", 1249 | "🇩🇰": "flag_dk", 1250 | "🇩🇯": "flag_dj", 1251 | "🇩🇲": "flag_dm", 1252 | "🇩🇴": "flag_do", 1253 | "🇪🇨": "flag_ec", 1254 | "🇪🇬": "flag_eg", 1255 | "🇸🇻": "flag_sv", 1256 | "🇬🇶": "flag_gq", 1257 | "🇪🇷": "flag_er", 1258 | "🇪🇪": "flag_ee", 1259 | "🇪🇹": "flag_et", 1260 | "🇫🇰": "flag_fk", 1261 | "🇫🇴": "flag_fo", 1262 | "🇫🇯": "flag_fj", 1263 | "🇫🇮": "flag_fi", 1264 | "🇫🇷": "flag_fr", 1265 | "🇵🇫": "flag_pf", 1266 | "🇬🇦": "flag_ga", 1267 | "🇬🇲": "flag_gm", 1268 | "🇬🇪": "flag_ge", 1269 | "🇩🇪": "flag_de", 1270 | "🇬🇭": "flag_gh", 1271 | "🇬🇮": "flag_gi", 1272 | "🇬🇷": "flag_gr", 1273 | "🇬🇱": "flag_gl", 1274 | "🇬🇩": "flag_gd", 1275 | "🇬🇺": "flag_gu", 1276 | "🇬🇹": "flag_gt", 1277 | "🇬🇳": "flag_gn", 1278 | "🇬🇼": "flag_gw", 1279 | "🇬🇾": "flag_gy", 1280 | "🇭🇹": "flag_ht", 1281 | "🇭🇳": "flag_hn", 1282 | "🇭🇰": "flag_hk", 1283 | "🇭🇺": "flag_hu", 1284 | "🇮🇸": "flag_is", 1285 | "🇮🇳": "flag_in", 1286 | "🇮🇩": "flag_id", 1287 | "🇮🇷": "flag_ir", 1288 | "🇮🇶": "flag_iq", 1289 | "🇮🇪": "flag_ie", 1290 | "🇮🇱": "flag_il", 1291 | "🇮🇹": "flag_it", 1292 | "🇨🇮": "flag_ci", 1293 | "🇯🇲": "flag_jm", 1294 | "🇯🇵": "flag_jp", 1295 | "🇯🇪": "flag_je", 1296 | "🇯🇴": "flag_jo", 1297 | "🇰🇿": "flag_kz", 1298 | "🇰🇪": "flag_ke", 1299 | "🇰🇮": "flag_ki", 1300 | "🇽🇰": "flag_xk", 1301 | "🇰🇼": "flag_kw", 1302 | "🇰🇬": "flag_kg", 1303 | "🇱🇦": "flag_la", 1304 | "🇱🇻": "flag_lv", 1305 | "🇱🇧": "flag_lb", 1306 | "🇱🇸": "flag_ls", 1307 | "🇱🇷": "flag_lr", 1308 | "🇱🇾": "flag_ly", 1309 | "🇱🇮": "flag_li", 1310 | "🇱🇹": "flag_lt", 1311 | "🇱🇺": "flag_lu", 1312 | "🇲🇴": "flag_mo", 1313 | "🇲🇰": "flag_mk", 1314 | "🇲🇬": "flag_mg", 1315 | "🇲🇼": "flag_mw", 1316 | "🇲🇾": "flag_my", 1317 | "🇲🇻": "flag_mv", 1318 | "🇲🇱": "flag_ml", 1319 | "🇲🇹": "flag_mt", 1320 | "🇲🇭": "flag_mh", 1321 | "🇲🇷": "flag_mr", 1322 | "🇲🇺": "flag_mu", 1323 | "🇲🇽": "flag_mx", 1324 | "🇫🇲": "flag_fm", 1325 | "🇲🇩": "flag_md", 1326 | "🇲🇨": "flag_mc", 1327 | "🇲🇳": "flag_mn", 1328 | "🇲🇪": "flag_me", 1329 | "🇲🇸": "flag_ms", 1330 | "🇲🇦": "flag_ma", 1331 | "🇲🇿": "flag_mz", 1332 | "🇲🇲": "flag_mm", 1333 | "🇳🇦": "flag_na", 1334 | "🇳🇷": "flag_nr", 1335 | "🇳🇵": "flag_np", 1336 | "🇳🇱": "flag_nl", 1337 | "🇳🇨": "flag_nc", 1338 | "🇳🇿": "flag_nz", 1339 | "🇳🇮": "flag_ni", 1340 | "🇳🇪": "flag_ne", 1341 | "🇳🇬": "flag_ng", 1342 | "🇳🇺": "flag_nu", 1343 | "🇰🇵": "flag_kp", 1344 | "🇳🇴": "flag_no", 1345 | "🇴🇲": "flag_om", 1346 | "🇵🇰": "flag_pk", 1347 | "🇵🇼": "flag_pw", 1348 | "🇵🇸": "flag_ps", 1349 | "🇵🇦": "flag_pa", 1350 | "🇵🇬": "flag_pg", 1351 | "🇵🇾": "flag_py", 1352 | "🇵🇪": "flag_pe", 1353 | "🇵🇭": "flag_ph", 1354 | "🇵🇱": "flag_pl", 1355 | "🇵🇹": "flag_pt", 1356 | "🇵🇷": "flag_pr", 1357 | "🇶🇦": "flag_qa", 1358 | "🇷🇴": "flag_ro", 1359 | "🇷🇺": "flag_ru", 1360 | "🇷🇼": "flag_rw", 1361 | "🇸🇭": "flag_sh", 1362 | "🇰🇳": "flag_kn", 1363 | "🇱🇨": "flag_lc", 1364 | "🇻🇨": "flag_vc", 1365 | "🇼🇸": "flag_ws", 1366 | "🇸🇲": "flag_sm", 1367 | "🇸🇹": "flag_st", 1368 | "🇸🇦": "flag_sa", 1369 | "🇸🇳": "flag_sn", 1370 | "🇷🇸": "flag_rs", 1371 | "🇸🇨": "flag_sc", 1372 | "🇸🇱": "flag_sl", 1373 | "🇸🇬": "flag_sg", 1374 | "🇸🇰": "flag_sk", 1375 | "🇸🇮": "flag_si", 1376 | "🇸🇧": "flag_sb", 1377 | "🇸🇴": "flag_so", 1378 | "🇿🇦": "flag_za", 1379 | "🇰🇷": "flag_kr", 1380 | "🇪🇸": "flag_es", 1381 | "🇱🇰": "flag_lk", 1382 | "🇸🇩": "flag_sd", 1383 | "🇸🇷": "flag_sr", 1384 | "🇸🇿": "flag_sz", 1385 | "🇸🇪": "flag_se", 1386 | "🇨🇭": "flag_ch", 1387 | "🇸🇾": "flag_sy", 1388 | "🇹🇼": "flag_tw", 1389 | "🇹🇯": "flag_tj", 1390 | "🇹🇿": "flag_tz", 1391 | "🇹🇭": "flag_th", 1392 | "🇹🇱": "flag_tl", 1393 | "🇹🇬": "flag_tg", 1394 | "🇹🇴": "flag_to", 1395 | "🇹🇹": "flag_tt", 1396 | "🇹🇳": "flag_tn", 1397 | "🇹🇷": "flag_tr", 1398 | "🇹🇲": "flag_tm", 1399 | "🇹🇻": "flag_tv", 1400 | "🇺🇬": "flag_ug", 1401 | "🇺🇦": "flag_ua", 1402 | "🇦🇪": "flag_ae", 1403 | "🇬🇧": "flag_gb", 1404 | "🇺🇸": "flag_us", 1405 | "🇻🇮": "flag_vi", 1406 | "🇺🇾": "flag_uy", 1407 | "🇺🇿": "flag_uz", 1408 | "🇻🇺": "flag_vu", 1409 | "🇻🇦": "flag_va", 1410 | "🇻🇪": "flag_ve", 1411 | "🇻🇳": "flag_vn", 1412 | "🇼🇫": "flag_wf", 1413 | "🇪🇭": "flag_eh", 1414 | "🇾🇪": "flag_ye", 1415 | "🇿🇲": "flag_zm", 1416 | "🇿🇼": "flag_zw", 1417 | "🇷🇪": "flag_re", 1418 | "🇦🇽": "flag_ax", 1419 | "🇹🇦": "flag_ta", 1420 | "🇮🇴": "flag_io", 1421 | "🇧🇶": "flag_bq", 1422 | "🇨🇽": "flag_cx", 1423 | "🇨🇨": "flag_cc", 1424 | "🇬🇬": "flag_gg", 1425 | "🇮🇲": "flag_im", 1426 | "🇾🇹": "flag_yt", 1427 | "🇳🇫": "flag_nf", 1428 | "🇵🇳": "flag_pn", 1429 | "🇧🇱": "flag_bl", 1430 | "🇵🇲": "flag_pm", 1431 | "🇬🇸": "flag_gs", 1432 | "🇹🇰": "flag_tk", 1433 | "🇧🇻": "flag_bv", 1434 | "🇭🇲": "flag_hm", 1435 | "🇸🇯": "flag_sj", 1436 | "🇺🇲": "flag_um", 1437 | "🇮🇨": "flag_ic", 1438 | "🇪🇦": "flag_ea", 1439 | "🇨🇵": "flag_cp", 1440 | "🇩🇬": "flag_dg", 1441 | "🇦🇸": "flag_as", 1442 | "🇦🇶": "flag_aq", 1443 | "🇻🇬": "flag_vg", 1444 | "🇨🇰": "flag_ck", 1445 | "🇨🇼": "flag_cw", 1446 | "🇪🇺": "flag_eu", 1447 | "🇬🇫": "flag_gf", 1448 | "🇹🇫": "flag_tf", 1449 | "🇬🇵": "flag_gp", 1450 | "🇲🇶": "flag_mq", 1451 | "🇲🇵": "flag_mp", 1452 | "🇸🇽": "flag_sx", 1453 | "🇸🇸": "flag_ss", 1454 | "🇹🇨": "flag_tc", 1455 | "🇲🇫": "flag_mf", 1456 | "🏳️‍🌈": "gay_pride_flag", 1457 | } 1458 | -------------------------------------------------------------------------------- /bot/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/base64" 5 | "path" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | ) 12 | 13 | var mdReplacer = strings.NewReplacer( 14 | "_", "\\_", 15 | "*", "\\*", 16 | "`", "\\`", 17 | "~", "\\`", 18 | ) 19 | 20 | // Emoji represents a Discord emoji 21 | type Emoji struct { 22 | ID string `json:"id,omitempty"` 23 | Name string `json:"name"` 24 | Unicode string `json:"unicode,omitempty"` 25 | Animated bool `json:"animated"` 26 | } 27 | 28 | // String returns the emoji as a Discord string 29 | func (e *Emoji) String() string { 30 | if e.Unicode != "" { 31 | return e.Unicode 32 | } 33 | 34 | str := "<" 35 | 36 | if e.Animated { 37 | str += "a" 38 | } 39 | 40 | return str + ":" + e.Name + ":" + e.ID + ">" 41 | } 42 | 43 | // URL returns the emoji's image URL 44 | func (e *Emoji) URL() string { 45 | if e.ID == "" { 46 | return "" 47 | } 48 | 49 | url := "https://cdn.discordapp.com/emojis/" + e.ID 50 | 51 | if e.Animated { 52 | url += ".gif" 53 | } else { 54 | url += ".png" 55 | } 56 | 57 | return url 58 | } 59 | 60 | // API returns the emoji's API identifier 61 | func (e *Emoji) API() string { 62 | if e.Unicode != "" { 63 | return e.Unicode 64 | } 65 | 66 | return e.Name + ":" + e.ID 67 | } 68 | 69 | // EscapeMarkdown escapes Discord markdown 70 | func EscapeMarkdown(str string) string { 71 | return mdReplacer.Replace(str) 72 | } 73 | 74 | // PanicIf panics if err != nil 75 | func PanicIf(err error) { 76 | if err != nil { 77 | panic(err) 78 | } 79 | } 80 | 81 | // ParseID extracts the ID of a token 82 | func ParseID(token string) string { 83 | var t string 84 | 85 | if words := strings.Split(token, " "); len(words) == 2 { 86 | t = words[1] 87 | } else { 88 | t = words[0] 89 | } 90 | 91 | s, _ := base64.StdEncoding.DecodeString(t) 92 | return string(s) 93 | } 94 | 95 | // ParseEmoji parses an emoji 96 | func ParseEmoji(emoji string) *Emoji { 97 | emoji = strings.TrimSpace(emoji) 98 | 99 | if name, ok := emojis[emoji]; ok { 100 | return &Emoji{ 101 | Name: name, 102 | Unicode: emoji, 103 | } 104 | } 105 | 106 | matches := reCustomEmoji.FindStringSubmatch(emoji) 107 | if matches == nil { 108 | return nil 109 | } 110 | 111 | return &Emoji{ 112 | ID: matches[3], 113 | Name: matches[2], 114 | Animated: matches[1] == "a", 115 | } 116 | } 117 | 118 | // GetMissing gets missing permissions and places them in a codeblock 119 | func GetMissing(have, want int, l func(string, ...interface{}) string) string { 120 | missing := want &^ have 121 | perms := "```diff\n" 122 | 123 | for flag, permission := range Permissions { 124 | if missing&flag == flag { 125 | perms += "- " + l("permissions."+permission) + "\n" 126 | } 127 | } 128 | 129 | return perms + "```" 130 | } 131 | 132 | // GetImage gets the image attached to a message 133 | func GetImage(m *discordgo.Message) string { 134 | for _, a := range m.Attachments { 135 | if a.Width != 0 && isEmbeddable(a.Filename) { 136 | return a.URL 137 | } 138 | } 139 | 140 | for _, e := range m.Embeds { 141 | switch e.Type { 142 | case "image": 143 | return e.URL 144 | 145 | case "rich": 146 | if e.Image != nil && e.Image.Width != 0 { 147 | return e.Image.URL 148 | } 149 | if e.Thumbnail != nil && e.Thumbnail.Width != 0 { 150 | return e.Thumbnail.URL 151 | } 152 | } 153 | } 154 | 155 | return "" 156 | } 157 | 158 | // GetContent gets the content of a message 159 | func GetContent(m *discordgo.Message) (content string) { 160 | for _, e := range m.Embeds { 161 | if e.Type != "rich" { 162 | continue 163 | } 164 | 165 | if e.Description != "" { 166 | content = e.Description 167 | } 168 | } 169 | 170 | if content == "" { 171 | content = m.Content 172 | } 173 | 174 | if len(m.Attachments) != 0 { 175 | files := make([]string, 0) 176 | 177 | for _, a := range m.Attachments { 178 | if !isEmbeddable(a.Filename) { 179 | files = append(files, "[["+a.Filename+"]]("+a.URL+")") 180 | } 181 | } 182 | 183 | if len(files) != 0 { 184 | fileStr := strings.Join(files, " ") 185 | if content != "" { 186 | fileStr = "\n\n" + fileStr 187 | } 188 | 189 | if len(content)+len(fileStr) <= 2048 { 190 | content += fileStr 191 | } 192 | } 193 | } 194 | 195 | return 196 | } 197 | 198 | // StartsWithEmoji checks whether the first character of a string contains an emoji 199 | func StartsWithEmoji(str string) bool { 200 | _, ok := emojis[string([]rune(str)[0])] 201 | return ok 202 | } 203 | 204 | const ( 205 | discordSnowflakeEpoch = 1420070400000 206 | snowflakeTimestampShift = 22 207 | ) 208 | 209 | // SnowflakeTimestamp gets the timestamp of a Discord snowflake 210 | func SnowflakeTimestamp(snowflake string) time.Time { 211 | id, err := strconv.ParseInt(snowflake, 10, 64) 212 | if err != nil { 213 | return time.Now() 214 | } 215 | 216 | return time.Unix(0, ((id>>snowflakeTimestampShift)+discordSnowflakeEpoch)*int64(time.Millisecond)) 217 | } 218 | 219 | func isEmbeddable(filename string) bool { 220 | switch strings.ToLower(path.Ext(filename)) { 221 | case ".png", ".jpg", ".jpeg", ".gif", ".webp": 222 | return true 223 | } 224 | 225 | return false 226 | } 227 | -------------------------------------------------------------------------------- /bot/utils.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/bwmarrin/discordgo" 9 | "github.com/xdimgg/starboard/bot/util" 10 | ) 11 | 12 | func findDefaultChannel(key string, state *discordgo.State, guild *discordgo.Guild) *discordgo.Channel { 13 | for _, channel := range guild.Channels { 14 | switch { 15 | case channel.Type != discordgo.ChannelTypeGuildText, 16 | key == settingNSFWChannel && !channel.NSFW, 17 | !strings.Contains(channel.Name, "starboard"): 18 | default: 19 | perms, err := state.UserChannelPermissions(state.User.ID, channel.ID) 20 | if err == nil && perms&discordgo.PermissionSendMessages == discordgo.PermissionSendMessages { 21 | return channel 22 | } 23 | } 24 | } 25 | 26 | return nil 27 | } 28 | 29 | func getSettingString(key string, value interface{}) string { 30 | if strings.Contains(key, settingChannel) && value != settingNone { 31 | if str, ok := value.(string); ok { 32 | value = "<#" + str + ">" 33 | } 34 | } 35 | 36 | if key == settingLanguage { 37 | value = util.Languages[value.(string)] 38 | } 39 | 40 | if key == settingRandomStarProbability { 41 | return strconv.FormatFloat(value.(float64), 'f', -1, 64) + "%" 42 | } 43 | 44 | return fmt.Sprintf("%v", value) 45 | } 46 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | bot: 5 | build: . 6 | restart: always 7 | env_file: env/prod.env 8 | depends_on: 9 | - postgres 10 | - redis 11 | redis: 12 | image: redis:alpine 13 | env_file: env/prod.env 14 | postgres: 15 | image: postgres:alpine 16 | env_file: env/prod.env 17 | volumes: 18 | - pgdata:/var/lib/postgresql/data 19 | 20 | volumes: 21 | pgdata: -------------------------------------------------------------------------------- /env/prod.example.env: -------------------------------------------------------------------------------- 1 | # Bot 2 | BOT_PREFIX=* 3 | BOT_TOKEN= 4 | BOT_LOCALES=/go/src/github.com/xdimgg/starboard/locales 5 | BOT_OWNER_ID= 6 | MODE=prod 7 | 8 | # Bot logs 9 | BOT_GUILD= 10 | BOT_GUILD_LOG_CHANNEL= 11 | BOT_MEMBER_LOG_CHANNEL= 12 | SENTRY_DSN= 13 | 14 | # Bot lists 15 | DBL_KEY= 16 | PW_BOTS_KEY= 17 | 18 | # Postgres 19 | POSTGRES_ADDR=postgres:5432 20 | POSTGRES_DB=postgres 21 | POSTGRES_USER=postgres 22 | POSTGRES_PASSWORD=postgres 23 | POSTGRES_POOL_SIZE= 24 | 25 | # Redis 26 | REDIS_ADDR=redis:6379 27 | REDIS_PASSWORD= 28 | REDIS_POOL_SIZE= -------------------------------------------------------------------------------- /locales/README.md: -------------------------------------------------------------------------------- 1 | # Localization Guide 2 | If you're planning on writing translations, there are just a couple of things to know 3 | 4 | ### Don't include the following command(s) (since only the owner can use them) 5 | - reload-locales 6 | 7 | ### How to localize lists properly 8 | The 4 fields related to lists are in the following format: 9 | ```json 10 | "list.or.prefix": "either ", 11 | "list.or.double": "%s or %s", 12 | "list.or.seperator": ", ", 13 | "list.or.final_seperator": ", or ", 14 | ``` 15 | 16 | The pseudocode for generating lists is as follows (assuming `items` is a string array and `locale` is a function that gets the locale definition of the provided key and executes C style string formatting using the provided arguments): 17 | ```py 18 | def locale_list(items): 19 | if len(items) == 1: 20 | return items[0] 21 | 22 | result = locale('list.or.prefix') 23 | 24 | if len(items) == 2: 25 | return result + locale('list.or.double', items[0], items[1]) 26 | 27 | for index, item in enumerate(items): 28 | result += item 29 | 30 | if index == len(items) - 1: pass # Ignore last item 31 | elif index == len(items) - 2: result += locale('list.or.final_seperator') # Insert final seperator at second to last item 32 | else: result += locale('list.or.seperator') # Insert seperator at every other item 33 | 34 | return result 35 | ``` 36 | 37 | ### How `settings.to_key.*` works 38 | Whenever a user runs the settings command like so: `@Starboard settings asdf`, the program will attempt to look at the value for `settings.to_key.asdf` to know which setting the user is referring to. -------------------------------------------------------------------------------- /locales/de-DE.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "Während der Ausführung des Befehls ist ein Fehler aufgetreten. Mein Besitzer schaut sich das an sobald er kann!", 3 | 4 | "list.or.prefix": "entweder ", 5 | "list.or.double": "%s oder %s", 6 | "list.or.seperator": ", ", 7 | "list.or.final_seperator": ", oder ", 8 | 9 | "message.content": "Inhalt", 10 | "message.author": "Autor", 11 | "message.channel": "Kanal", 12 | 13 | "starboard.self_star.warning": "%s, du kannst deiner eigenen Nachricht keinen Stern geben.", 14 | 15 | "permissions.CREATE_INSTANT_INVITE": "Soforteinladung erstellen", 16 | "permissions.KICK_MEMBERS": "Mitglieder kicken", 17 | "permissions.BAN_MEMBERS": "Mitglieder bannen", 18 | "permissions.ADMINISTRATOR": "Administrator", 19 | "permissions.MANAGE_CHANNELS": "Kanäle verwalten", 20 | "permissions.MANAGE_GUILD": "Server verwalten", 21 | "permissions.ADD_REACTIONS": "Reaktionen hinzufügen", 22 | "permissions.VIEW_AUDIT_LOG": "Audit-Log anzeigen", 23 | "permissions.VIEW_CHANNEL": "Textkanäle lesen und Sprachkanäle sehen", 24 | "permissions.SEND_MESSAGES": "Nachrichten versenden", 25 | "permissions.SEND_TTS_MESSAGES": "TTS-Nachrichten senden", 26 | "permissions.MANAGE_MESSAGES": "Nachrichten verwalten", 27 | "permissions.EMBED_LINKS": "Links einbetten", 28 | "permissions.ATTACH_FILES": "Dateien anhängen", 29 | "permissions.READ_MESSAGE_HISTORY": "Nachrichtenverlauf lesen", 30 | "permissions.MENTION_EVERYONE": "Alle erwähnen", 31 | "permissions.USE_EXTERNAL_EMOJIS": "Externe Emojis verwenden", 32 | "permissions.CONNECT": "Verbinden", 33 | "permissions.SPEAK": "Sprechen", 34 | "permissions.MUTE_MEMBERS": "Mitglieder stummschalten", 35 | "permissions.DEAFEN_MEMBERS": "Mitglieder taub schalten", 36 | "permissions.MOVE_MEMBERS": "Mitglieder verschieben", 37 | "permissions.USE_VAD": "Sprachaktivierung verwenden", 38 | "permissions.PRIORITY_SPEAKER": "Prioritätssprecher", 39 | "permissions.CHANGE_NICKNAME": "Nickname ändern", 40 | "permissions.MANAGE_NICKNAMES": "Nicknamen verwalten", 41 | "permissions.MANAGE_ROLES": "Rollen verwalten", 42 | "permissions.MANAGE_WEBHOOKS": "WebHooks verwalten", 43 | "permissions.MANAGE_EMOJIS": "Emojis verwalten", 44 | 45 | "restrictions.guild_only": "Dieser Befehl kann nur in Servern verwendet werden.", 46 | "restrictions.owner_only": "Dieser Befehl kann nur von meinem Besitzer verwendet werden.", 47 | "restrictions.permissions.missing.client": "Mir fehlen die folgenden Berechtigungen:\n%s", 48 | "restrictions.permissions.missing.member": "Dir fehlen die folgenden Berechtigungen:\n%s", 49 | "restrictions.permissions.missing.member.error": "Ich konnte deine Berechtigungen nicht feststellen, stell sicher, dass du online bist.", 50 | 51 | "commands.ping.name": "ping", 52 | "commands.ping.description": "Teste die Verbindung des Bots zu Discord.", 53 | "commands.ping.phrases.pinging": "ping...", 54 | "commands.ping.phrases.done": "Pong!\nAntwortzeit: %dms", 55 | 56 | "commands.help.name": "hilfe", 57 | "commands.help.description": "Zeigt und erklärt alle Befehle.", 58 | "commands.help.phrase.commands": "Befehle", 59 | "commands.help.phrase.aliases": "Alternative Namen", 60 | 61 | "commands.setup.name": "Einrichtung", 62 | "commands.setup.usage": "[nsfw]", 63 | "commands.setup.nsfw": "nsfw", 64 | "commands.setup.description": "Erstellt einen Sternbrettkanal (optional NSFW) mit den entsprechenden Berechtigungen.", 65 | "commands.setup.phrase.exists": "Es gibt bereits einen Sternbrettkanal in %s.", 66 | "commands.setup.phrase.done": "Ein Sternbrettkanal wurde in %s erstellt.", 67 | 68 | "commands.fix.name": "berichtigen", 69 | "commands.fix.usage": "{Nachrichten ID oder Nachrichten Verlinkung}", 70 | "commands.fix.description": "Berichtigt die Sternenanzahl einer Nachricht.", 71 | "commands.fix.phrase.id": "Ungültige/r Nachrichten ID/Link angegeben.", 72 | "commands.fix.phrase.permissions": "Ich kann in diesem Kanal keine Nachrichten lesen.", 73 | "commands.fix.phrase.unknown_message": "Ich habe keine Nachricht mit dieser ID/ diesem Link gefunden.", 74 | "commands.fix.phrase.done": "Nachricht berichtigt.", 75 | 76 | "commands.stats.name": "statistiken", 77 | "commands.stats.description": "Zeigt einige Statistiken.", 78 | "commands.stats.phrase.title": "Statistiken", 79 | "commands.stats.phrase.system": "System", 80 | "commands.stats.phrase.system_value": "• Betriebszeit: %s\n• Speicher: %s\n• CPUs: %s\n• [Go](https://golang.org): %s\n• [DiscordGo](https://github.com/bwmarrin/discordgo): %s", 81 | "commands.stats.phrase.bot": "Bot", 82 | "commands.stats.phrase.bot_value": "• Fragmente: %s\n• Server: %s\n• Kanäle: %s\n• Mitglieder: %s", 83 | "commands.stats.phrase.starboard": "Sternbrett", 84 | "commands.stats.phrase.starboard_value": "• Nachrichten: %s\n• Sterne: %s", 85 | 86 | "commands.invite.name": "einladen", 87 | "commands.invite.aliases": ["hilfe", "unterstützen"], 88 | "commands.invite.description": "Zeigt nützliche Links an.", 89 | "commands.invite.phrase.content": "Mit [diesem Link](%s) kannst du mich zu deinem Server hinzufügen.\nDu kannst meinem Heimserver beitreten um nach Hilfe zu fragen oder einfach Hallo zu sagen indem du [diesem Link](%S) folgst.\nDu kannst dieses Projekt auf [Patreon](%s) oder [PayPal](%s) understützen oder [mich auf Discord Bot List wählen](%s), damit mehr Leute diesen Bot finden.", 90 | 91 | "commands.block.name": "blocken", 92 | "commands.block.usage": "[hinzufügen|entfernen] [@Mitglied|#Kanal|alle]...", 93 | "commands.block.aliases": ["whitelist", "blacklist", "blocks"], 94 | "commands.block.description": "Blockt ein Mitglied oder einen Kanal.", 95 | "commands.block.phrase.action": "Aktion", 96 | "commands.block.phrase.missing": "Du musst mindestens ein Mitglied oder einen Kanal angeben.", 97 | "commands.block.phrase.add": "hinzufügen", 98 | "commands.block.phrase.remove": "entfernen", 99 | "commands.block.phrase.all": "alle", 100 | "commands.block.phrase.none": "Keine", 101 | "commands.block.phrase.users": "Mitglieder", 102 | "commands.block.phrase.channels": "Kanäle", 103 | "commands.block.phrase.roles": "Rollen", 104 | 105 | "commands.leaderboard.name": "bestenliste", 106 | "commands.leaderboard.usage": "[Seitenzahl]", 107 | "commands.leaderboard.description": "Zeige eine Liste der Mitglieder mit den meisten Sternen.", 108 | "commands.leaderboard.phrase.empty": "Es gibt keine Seite zum Anzeigen.", 109 | "commands.leaderboard.phrase.number": "Seitenzahl muss eine Nummer sein.", 110 | "commands.leaderboard.phrase.min": "Seitenzahl kann nicht kleiner als %d sein.", 111 | "commands.leaderboard.phrase.max": "Seitenzahl kann nicht größer als %d sein.", 112 | "commands.leaderboard.phrase.page": "Seite %d von %d.", 113 | 114 | "commands.config.name": "konfigurieren", 115 | "commands.config.usage": "[Einstellung] [neuer Wert]", 116 | "commands.config.description": "Ändert oder zeigt Serverweite Einstellungen.", 117 | "commands.config.aliases": ["einstellung", "einstellungen", "konfig"], 118 | 119 | "settings.restrictions.max_length": "%s darf nicht mehr als %d Zeichen haben.", 120 | "settings.restrictions.one_of": "%s muss %s sein", 121 | "settings.restrictions.number": "%s muss eine Zahl sein.", 122 | "settings.restrictions.min": "%s darf nicht weniger als %d sein.", 123 | "settings.restrictions.max": "%s darf nicht größer als %d sein.", 124 | "settings.restrictions.min_percentage": "%s darf nicht weniger als %f%% sein.", 125 | "settings.restrictions.max_percentage": "%s darf nicht größer als %f%%. sein", 126 | "settings.restrictions.emoji": "%s muss ein gültiges Emoji oder Discord Emoji sein.", 127 | "settings.restrictions.channel": "%s muss ein gültiger Discord Kanal sein.", 128 | "settings.restrictions.channel_perms": "Ich kann diesen Kanal nicht lesen.", 129 | "settings.restrictions.channel_nsfw": "Dieser Kanal muss NSFW aktiviert haben.", 130 | 131 | "settings.phrase.unknown": "Diese Einstellung gibt es nicht.", 132 | "settings.phrase.updated": "%s wurde geändert.", 133 | "settings.phrase.mode": "Modus: %s", 134 | "settings.phrase.true": "ja", 135 | "settings.phrase.false": "nein", 136 | "settings.phrase.blacklist": "Blacklist", 137 | "settings.phrase.whitelist": "Whitelist", 138 | 139 | "settings.prefix": "Prefix", 140 | "settings.language": "Sprache", 141 | "settings.minimum": "Minimum", 142 | "settings.self_star": "Selbst-Stern", 143 | "settings.self_star_warning": "Selbst-Stern-Warnung", 144 | "settings.emoji": "Emoji", 145 | "settings.channel": "Kanal", 146 | "settings.nsfw_channel": "NSFW-Kanal", 147 | "settings.minimal": "Minimal", 148 | "settings.remove_bot_stars": "Entferne-Bot-Sterne", 149 | "settings.save_deleted_messages": "Speichere-gelöschte-Nachrichten", 150 | "settings.block_mode": "Block-Modus", 151 | "settings.random_star_probability": "Zufällige-Stern-Wahrscheinlichkeit", 152 | 153 | "settings.to_key.prefix": "prefix", 154 | "settings.to_key.sprache": "language", 155 | "settings.to_key.minimum": "minimum", 156 | "settings.to_key.selbststern": "self_star", 157 | "setinngs.to_key.selbststernwarnung": "self_star_warning", 158 | "settings.to_key.emoji": "emoji", 159 | "settings.to_key.kanal": "channel", 160 | "settings.to_key.nsfwkanal": "nsfw_channel", 161 | "settings.to_key.minimal": "minimal", 162 | "settings.to_key.entfernebotsterne": "remove_bot_stars", 163 | "settings.to_key.speicheregelöschtenachrichten": "save_deleted_messages", 164 | "settings.to_key.blockmodus": "block_mode", 165 | "settings.to_key.zufälligesternwahrscheinlichkeit": "random_star_probability" 166 | } 167 | -------------------------------------------------------------------------------- /locales/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "An error occurred while executing this command. My owner will be looking into it as soon as they can!", 3 | 4 | "list.or.prefix": "either ", 5 | "list.or.double": "%s or %s", 6 | "list.or.seperator": ", ", 7 | "list.or.final_seperator": ", or ", 8 | 9 | "message.content": "Content", 10 | "message.author": "Author", 11 | "message.channel": "Channel", 12 | 13 | "starboard.self_star.warning": "%s, you can't star your own messages.", 14 | 15 | "permissions.CREATE_INSTANT_INVITE": "Create Instant Invite", 16 | "permissions.KICK_MEMBERS": "Kick Members", 17 | "permissions.BAN_MEMBERS": "Ban Members", 18 | "permissions.ADMINISTRATOR": "Administrator", 19 | "permissions.MANAGE_CHANNELS": "Manage Channels", 20 | "permissions.MANAGE_GUILD": "Manage Guild", 21 | "permissions.ADD_REACTIONS": "Add Reactions", 22 | "permissions.VIEW_AUDIT_LOG": "View Audit Log", 23 | "permissions.VIEW_CHANNEL": "View Channel", 24 | "permissions.SEND_MESSAGES": "Send Messages", 25 | "permissions.SEND_TTS_MESSAGES": "Send TTS Messages", 26 | "permissions.MANAGE_MESSAGES": "Manage Messages", 27 | "permissions.EMBED_LINKS": "Embed Links", 28 | "permissions.ATTACH_FILES": "Attach Files", 29 | "permissions.READ_MESSAGE_HISTORY": "Read Message History", 30 | "permissions.MENTION_EVERYONE": "Mention Everyone", 31 | "permissions.USE_EXTERNAL_EMOJIS": "Use External Emojis", 32 | "permissions.CONNECT": "Connect", 33 | "permissions.SPEAK": "Speak", 34 | "permissions.MUTE_MEMBERS": "Mute Members", 35 | "permissions.DEAFEN_MEMBERS": "Deafen Members", 36 | "permissions.MOVE_MEMBERS": "Move Members", 37 | "permissions.USE_VAD": "Use VAD", 38 | "permissions.PRIORITY_SPEAKER": "Priority Speaker", 39 | "permissions.CHANGE_NICKNAME": "Change Nickname", 40 | "permissions.MANAGE_NICKNAMES": "Manage Nicknames", 41 | "permissions.MANAGE_ROLES": "Manage Roles", 42 | "permissions.MANAGE_WEBHOOKS": "Manage Webhooks", 43 | "permissions.MANAGE_EMOJIS": "Manage Emojis", 44 | 45 | "restrictions.guild_only": "This command can only be run in servers.", 46 | "restrictions.owner_only": "This command can only be run by my owner.", 47 | "restrictions.permissions.missing.client": "I'm missing the following required permissions:\n%s", 48 | "restrictions.permissions.missing.member": "You're missing the following required permissions:\n%s", 49 | "restrictions.permissions.missing.member.error": "I couldn't get your permissions, make sure you're online.", 50 | 51 | "commands.ping.name": "ping", 52 | "commands.ping.description": "Tests bot's connection to Discord.", 53 | "commands.ping.phrases.pinging": "Pinging...", 54 | "commands.ping.phrases.done": "Pong!\nResponse time: %dms", 55 | 56 | "commands.reload-locales.name": "reload-locales", 57 | "commands.reload-locales.aliases": ["reloadlocales"], 58 | "commands.reload-locales.description": "Reads all locale files.", 59 | "commands.reload-locales.phrase.done": "Successfully read all locale files.", 60 | 61 | "commands.help.name": "help", 62 | "commands.help.description": "Lists and explains all commands.", 63 | "commands.help.phrase.commands": "Commands", 64 | "commands.help.phrase.aliases": "Aliases", 65 | 66 | "commands.setup.name": "setup", 67 | "commands.setup.usage": "[nsfw]", 68 | "commands.setup.nsfw": "nsfw", 69 | "commands.setup.description": "Creates a Starboard (optionally NSFW) channel with appropriate permissions.", 70 | "commands.setup.phrase.exists": "A Starboard channel already exists in %s.", 71 | "commands.setup.phrase.done": "A Starboard channel has been created in %s.", 72 | 73 | "commands.fix.name": "fix", 74 | "commands.fix.usage": "{message ID or message link}", 75 | "commands.fix.description": "Fixes a messages star count.", 76 | "commands.fix.phrase.id": "Invalid message ID/link provided.", 77 | "commands.fix.phrase.permissions": "I am not permitted to read that channels messages.", 78 | "commands.fix.phrase.unknown_message": "I could not find a message for the provided ID/link.", 79 | "commands.fix.phrase.done": "Message has been fixed.", 80 | 81 | "commands.stats.name": "stats", 82 | "commands.stats.description": "Shows some of my stats.", 83 | "commands.stats.phrase.title": "Stats", 84 | "commands.stats.phrase.system": "System", 85 | "commands.stats.phrase.system_value": "• Uptime: %s\n• Memory: %s\n• CPUs: %s\n• [Go](https://golang.org): %s\n• [DiscordGo](https://github.com/bwmarrin/discordgo): %s", 86 | "commands.stats.phrase.bot": "Bot", 87 | "commands.stats.phrase.bot_value": "• Shards: %s\n• Servers: %s\n• Channels: %s\n• Members: %s", 88 | "commands.stats.phrase.starboard": "Starboard", 89 | "commands.stats.phrase.starboard_value": "• Messages: %s\n• Stars: %s", 90 | 91 | "commands.invite.name": "invite", 92 | "commands.invite.aliases": ["support", "donate"], 93 | "commands.invite.description": "Provides some useful links.", 94 | "commands.invite.phrase.content": "You can add me to your server using [this](%s) link.\nYou can join my support server to ask for help or just say hi using [this](%s) link.\nYou can support this project via [Patreon](%s) or [PayPal](%s).\nYou can also [vote for me on Discord Bot List](%s) so more people can find this bot!", 95 | 96 | "commands.block.name": "block", 97 | "commands.block.usage": "[add|remove] [@user|#channel|all]...", 98 | "commands.block.aliases": ["whitelist", "blacklist", "blocks"], 99 | "commands.block.description": "Blocks a user or channel.", 100 | "commands.block.phrase.action": "Action", 101 | "commands.block.phrase.missing": "You must provide at least one user or channel.", 102 | "commands.block.phrase.add": "add", 103 | "commands.block.phrase.remove": "remove", 104 | "commands.block.phrase.all": "all", 105 | "commands.block.phrase.none": "None", 106 | "commands.block.phrase.users": "Users", 107 | "commands.block.phrase.channels": "Channels", 108 | "commands.block.phrase.roles": "Roles", 109 | 110 | "commands.leaderboard.name": "leaderboard", 111 | "commands.leaderboard.usage": "[page]", 112 | "commands.leaderboard.description": "Lists the top starred people.", 113 | "commands.leaderboard.phrase.empty": "There are no pages to show.", 114 | "commands.leaderboard.phrase.number": "Page must be a number.", 115 | "commands.leaderboard.phrase.min": "Page can't be lower than %d.", 116 | "commands.leaderboard.phrase.max": "Page can't be greater than %d.", 117 | "commands.leaderboard.phrase.page": "Page %d of %d.", 118 | 119 | "commands.config.name": "config", 120 | "commands.config.usage": "[setting] [new-value]", 121 | "commands.config.description": "Changes or shows server-wide settings.", 122 | "commands.config.aliases": ["setting", "settings"], 123 | 124 | "commands.troubleshoot.name": "troubleshoot", 125 | "commands.troubleshoot.description": "Runs a config audit that checks for common errors.", 126 | "commands.troubleshoot.aliases": ["audit"], 127 | "commands.troubleshoot.errors": "__Errors__", 128 | "commands.troubleshoot.warnings": "__Warnings__", 129 | "commands.troubleshoot.missing_channel": "This server has no Starboard channel. You can run `%s%s` to fix this.", 130 | "commands.troubleshoot.missing_nsfw_channel": "This server has %d NSFW channel but no NSFW starboard. You can run `%s%s %s` to fix this.", 131 | "commands.troubleshoot.missing_nsfw_channel_multiple": "This server has %d NSFW channels but no NSFW starboard. You can run `%s%s %s` to fix this.", 132 | "commands.troubleshoot.missing_permissions": "I am missing the `%s` permission for %s.", 133 | "commands.troubleshoot.passed": "All tests have passed. If you're still having issues, you should join my support server which can be found with `%s%s`.", 134 | 135 | "settings.restrictions.max_length": "%s can't be longer than %d characters.", 136 | "settings.restrictions.one_of": "%s must be %s.", 137 | "settings.restrictions.number": "%s must be a number.", 138 | "settings.restrictions.min": "%s can't be less than %d.", 139 | "settings.restrictions.max": "%s can't be greater than %d.", 140 | "settings.restrictions.min_percentage": "%s can't be less than %f%%.", 141 | "settings.restrictions.max_percentage": "%s can't be greater than %f%%.", 142 | "settings.restrictions.emoji": "%s must be a valid emoji or Discord emoji.", 143 | "settings.restrictions.channel": "%s must be a valid Discord channel.", 144 | "settings.restrictions.channel_perms": "I don't have access to that channel.", 145 | "settings.restrictions.channel_nsfw": "Channel must have NSFW enabled.", 146 | 147 | "settings.phrase.unknown": "Setting doesn't exist.", 148 | "settings.phrase.updated": "%s has been updated.", 149 | "settings.phrase.mode": "Mode: %s", 150 | "settings.phrase.true": "true", 151 | "settings.phrase.false": "false", 152 | "settings.phrase.blacklist": "blacklist", 153 | "settings.phrase.whitelist": "whitelist", 154 | 155 | "settings.prefix": "Prefix", 156 | "settings.language": "Language", 157 | "settings.minimum": "Minimum", 158 | "settings.self_star": "Self-star", 159 | "settings.self_star_warning": "Self-star-warning", 160 | "settings.emoji": "Emoji", 161 | "settings.channel": "Channel", 162 | "settings.nsfw_channel": "NSFW-channel", 163 | "settings.minimal": "Minimal", 164 | "settings.remove_bot_stars": "Remove-bot-stars", 165 | "settings.save_deleted_messages": "Save-deleted-messages", 166 | "settings.block_mode": "Block-mode", 167 | "settings.random_star_probability": "Random-star-probability", 168 | 169 | "settings.to_key.prefix": "prefix", 170 | "settings.to_key.language": "language", 171 | "settings.to_key.minimum": "minimum", 172 | "settings.to_key.selfstar": "self_star", 173 | "settings.to_key.selfstarwarning": "self_star_warning", 174 | "settings.to_key.emoji": "emoji", 175 | "settings.to_key.channel": "channel", 176 | "settings.to_key.nsfwchannel": "nsfw_channel", 177 | "settings.to_key.channelnsfw": "nsfw_channel", 178 | "settings.to_key.minimal": "minimal", 179 | "settings.to_key.removebotstars": "remove_bot_stars", 180 | "settings.to_key.savedeletedmessages": "save_deleted_messages", 181 | "settings.to_key.blockmode": "block_mode", 182 | "settings.to_key.starprobability": "random_star_probability", 183 | "settings.to_key.randomstar": "random_star_probability", 184 | "settings.to_key.randomstarprobability": "random_star_probability" 185 | } -------------------------------------------------------------------------------- /locales/nl-NL.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "Er is een fout opgetreden tijdens het uitvoeren van dit commando. Mijn eigenaar zal dit zo spoedig mogelijk nakijken!", 3 | 4 | "message.content": "Content", 5 | "message.author": "Auteur", 6 | "message.channel": "Kanaal", 7 | 8 | "permissions.CREATE_INSTANT_INVITE": "Creëer Directe Uitnodiging", 9 | "permissions.KICK_MEMBERS": "Verwijder Leden", 10 | "permissions.BAN_MEMBERS": "Verban Leden", 11 | "permissions.ADMINISTRATOR": "Administrator", 12 | "permissions.MANAGE_CHANNELS": "Beheer Kanalen", 13 | "permissions.MANAGE_GUILD": "Beheer Server", 14 | "permissions.ADD_REACTIONS": "Voeg Reacties Toe", 15 | "permissions.VIEW_AUDIT_LOG": "Bekijk Logboek", 16 | "permissions.VIEW_CHANNEL": "Bekijk Kanaal", 17 | "permissions.SEND_MESSAGES": "Berichten Versturen", 18 | "permissions.SEND_TTS_MESSAGES": "Verstuur TTS-berichten", 19 | "permissions.MANAGE_MESSAGES": "Beheer Berichten", 20 | "permissions.EMBED_LINKS": "Ingevoegde Links", 21 | "permissions.ATTACH_FILES": "Voeg Bijlagen Toe", 22 | "permissions.READ_MESSAGE_HISTORY": "Lees Berichtengeschiedenis", 23 | "permissions.MENTION_EVERYONE": "Vermeld Iedereen", 24 | "permissions.USE_EXTERNAL_EMOJIS": "Gebruik Externe Emoji", 25 | "permissions.CONNECT": "Verbind", 26 | "permissions.SPEAK": "Praat", 27 | "permissions.MUTE_MEMBERS": "Demp Lid", 28 | "permissions.DEAFEN_MEMBERS": "Demp Geluid voor Lid", 29 | "permissions.MOVE_MEMBERS": "Verplaats Lid", 30 | "permissions.USE_VAD": "Gebruik VAD", 31 | "permissions.PRIORITY_SPEAKER": "Prioriteitsspreker", 32 | "permissions.CHANGE_NICKNAME": "Verander Gebruikersnaam", 33 | "permissions.MANAGE_NICKNAMES": "Beheer Gebruikersnamen", 34 | "permissions.MANAGE_ROLES": "Beheer Rollen", 35 | "permissions.MANAGE_WEBHOOKS": "Beheer Webhooks", 36 | "permissions.MANAGE_EMOJIS": "Beheer Emoji", 37 | 38 | "restrictions.guild_only": "Dit commando kan alleen in servers gebruikt worden.", 39 | "restrictions.owner_only": "Dit commando kan alleen door de eigenaar gebruikt worden.", 40 | "restrictions.permissions.missing.client": "Ik mis de volgende vereiste rechten:\n%s", 41 | "restrictions.permissions.missing.member": "Je mist de volgende vereiste rechten:\n%s", 42 | "restrictions.permissions.missing.member.error": "Ik kon niet zien welke rechten je hebt. Zorg dat je online bent.", 43 | 44 | "commands.ping.name": "ping", 45 | "commands.ping.description": "Test de verbinding van de bot met Discord", 46 | "commands.ping.phrases.pinging": "Pingen...", 47 | "commands.ping.phrases.done": "Pong!\nReactietijd: %dms", 48 | 49 | "commands.help.name": "help", 50 | "commands.help.description": "Een lijst met alle commando's met uitleg.", 51 | "commands.help.phrase.commands": "Commando's", 52 | "commands.help.phrase.aliases": "Aliassen", 53 | 54 | "commands.fix.name": "oplossing", 55 | "commands.fix.usage": "{bericht-ID of bericht-link}", 56 | "commands.fix.description": "Lost problemen met de sterrentelling van een bericht op.", 57 | "commands.fix.phrase.id": "Ongeldige berichten-ID of -link opgegeven.", 58 | "commands.fix.phrase.permissions": "Ik heb geen toestemming om berichten van dat kanaal te lezen.", 59 | "commands.fix.phrase.done": "Problemen met berichten opgelost.", 60 | 61 | "commands.stats.name": "statistieken", 62 | "commands.stats.description": "Laat een aantal statistieken zien", 63 | "commands.stats.phrase.title": "Statistieken", 64 | "commands.stats.phrase.system": "Systeem", 65 | "commands.stats.phrase.system_value": "• Uptime: %s\n• Geheugen: %s\n• CPUs: %s\n• [Go](https://golang.org): %s\n• [DiscordGo](https://github.com/bwmarrin/discordgo): %s", 66 | "commands.stats.phrase.bot": "Bot", 67 | "commands.stats.phrase.bot_value": "• Shards: %s\n• Servers: %s\n• Channels: %s\n• Members: %s", 68 | "commands.stats.phrase.starboard": "Starboard", 69 | "commands.stats.phrase.starboard_value": "• Berichten: %s\n• Sterren: %s", 70 | 71 | "commands.invite.name": "inviteer", 72 | "commands.invite.aliases": ["support", "donate"], 73 | "commands.invite.description": "Geeft een aantal handige links.", 74 | "commands.invite.phrase.content": "Je kunt me aan je server toevoegen door [deze](%s) link te gebruiken.\nJe kunt mijn ondersteuningsserver bezoeken en vragen stellen of gewoon hallo zeggen met [deze](%s) link.\nJe kunt dit project steunen via [Patreon](%s) of [PayPal](%s).\nJe kunt ook [voor me stemmen op Discord Bot List](%s) zodat meer mensen deze bot kunnen vinden!", 75 | 76 | "commands.block.name": "blokkeer", 77 | "commands.block.usage": "[toevoegen|verwijderen] [@gebruiker|#kanaal|alles]...", 78 | "commands.block.aliases": ["whitelist", "blacklist", "blocks"], 79 | "commands.block.description": "Blokkeert een gebruiker of kanaal.", 80 | "commands.block.phrase.missing": "Je moet op z'n minst een kanaal of gebruiker opgeven.", 81 | "commands.block.phrase.add": "toevoegen", 82 | "commands.block.phrase.remove": "verwijderen", 83 | "commands.block.phrase.all": "alles", 84 | "commands.block.phrase.none": "Geen", 85 | "commands.block.phrase.users": "Gebruikers", 86 | "commands.block.phrase.channels": "Kanalen", 87 | "commands.block.phrase.roles": "Rollen", 88 | 89 | "commands.leaderboard.name": "scorebord", 90 | "commands.leaderboard.usage": "[page]", 91 | "commands.leaderboard.description": "Laat zien welke personen de meeste sterren hebben gekregen.", 92 | "commands.leaderboard.phrase.empty": "Er zijn geen pagina's om weer te geven.", 93 | "commands.leaderboard.phrase.number": "Pagina moet een nummer zijn.", 94 | "commands.leaderboard.phrase.min": "Pagina kan niet lager zijn dan %d.", 95 | "commands.leaderboard.phrase.max": "Pagina kan niet groter zijn dan %d.", 96 | "commands.leaderboard.phrase.page": "Pagina %d van %d.", 97 | 98 | "commands.config.name": "configuratie", 99 | "commands.config.usage": "[setting] [new-value]", 100 | "commands.config.description": "Verandert of geeft de serverwijde instellingen weer.", 101 | "commands.config.aliases": ["setting", "settings"], 102 | 103 | "settings.restrictions.max_length": "%s kan niet langer zijn dan %d tekens.", 104 | "settings.restrictions.one_of": "%s moet een van deze zijn: %s", 105 | "settings.restrictions.number": "%s must be a number.", 106 | "settings.restrictions.min": "%s kan niet minder zijn dan %d.", 107 | "settings.restrictions.max": "%s kan niet meer zijn dan %d.", 108 | "settings.restrictions.min_percentage": "%s kan niet minder zijn dan %f%%.", 109 | "settings.restrictions.max_percentage": "%s kan niet meer zijn dan %f%%.", 110 | "settings.restrictions.emoji": "%s moet een geldige Emoji of Discord-emoji zijn.", 111 | "settings.restrictions.channel": "%s moet een geldig Discord-kanaal zijn.", 112 | "settings.restrictions.channel_perms": "Ik heb geen toegang tot dat kanaal.", 113 | "settings.restrictions.channel_nsfw": "Kanaal moet NSFW ingeschakeld hebben.", 114 | 115 | "settings.phrase.unknown": "Instelling bestaat niet.", 116 | "settings.phrase.updated": "%s is zojuist geüpdatet.", 117 | "settings.phrase.mode": "Modus: %s", 118 | "settings.phrase.true": "waar", 119 | "settings.phrase.false": "onwaar", 120 | "settings.phrase.blacklist": "blacklist", 121 | "settings.phrase.whitelist": "whitelist", 122 | 123 | "settings.prefix": "Prefix", 124 | "settings.language": "Taal", 125 | "settings.minimum": "Minimum", 126 | "settings.self_star": "Zelf-ster", 127 | "settings.emoji": "Emoji", 128 | "settings.channel": "Kanaal", 129 | "settings.nsfw_channel": "NSFW-kanaal", 130 | "settings.minimal": "Minimaal", 131 | "settings.remove_bot_stars": "Verwijder-bot-sterren", 132 | "settings.save_deleted_messages": "Sla-verwijderde-berichten-op", 133 | "settings.block_mode": "Blokkeer-modus", 134 | 135 | "settings.to_key.prefix": "prefix", 136 | "settings.to_key.taal": "language", 137 | "settings.to_key.minimum": "minimum", 138 | "settings.to_key.zelfster": "self_star", 139 | "settings.to_key.emoji": "emoji", 140 | "settings.to_key.kanaal": "channel", 141 | "settings.to_key.nsfwkanaal": "nsfw_channel", 142 | "settings.to_key.minimaal": "minimal", 143 | "settings.to_key.verwijderbotsterren": "remove_bot_stars", 144 | "settings.to_key.slaverwijderdeberichtenop": "save_deleted_messages", 145 | "settings.to_key.blockmode": "block_mode" 146 | } 147 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/xdimgg/starboard/bot" 10 | 11 | "github.com/go-pg/pg" 12 | 13 | "github.com/go-redis/redis" 14 | ) 15 | 16 | func getenvInt(key string) int { 17 | if n, err := strconv.Atoi(os.Getenv(key)); err == nil { 18 | return n 19 | } 20 | 21 | return 0 22 | } 23 | 24 | func main() { 25 | if os.Getenv("MODE") == "prod" { 26 | time.Sleep(time.Second * 10) 27 | } 28 | 29 | panic(bot.New( 30 | &bot.Options{ 31 | Prefix: os.Getenv("BOT_PREFIX"), 32 | Token: os.Getenv("BOT_TOKEN"), 33 | Locales: os.Getenv("BOT_LOCALES"), 34 | OwnerID: os.Getenv("BOT_OWNER_ID"), 35 | Mode: os.Getenv("MODE"), 36 | SentryDSN: os.Getenv("SENTRY_DSN"), 37 | DiscordLists: []bot.DiscordList{ 38 | { 39 | Authorization: os.Getenv("DBL_KEY"), 40 | URL: func(id string) string { 41 | return "https://discordbots.org/api/bots/" + id + "/stats" 42 | }, 43 | Serialize: func(shardCount, guildCount int) ([]byte, error) { 44 | return json.Marshal(map[string]int{ 45 | "shard_count": shardCount, 46 | "server_count": guildCount, 47 | }) 48 | }, 49 | }, 50 | { 51 | Authorization: os.Getenv("GG_BOTS_KEY"), 52 | URL: func(id string) string { 53 | return "https://discord.bots.gg/api/v1/bots/" + id + "/stats" 54 | }, 55 | Serialize: func(shardCount, guildCount int) ([]byte, error) { 56 | return json.Marshal(map[string]int{ 57 | "shardCount": shardCount, 58 | "guildCount": guildCount, 59 | }) 60 | }, 61 | }, 62 | }, 63 | Guild: os.Getenv("BOT_GUILD"), 64 | GuildLogChannel: os.Getenv("BOT_GUILD_LOG_CHANNEL"), 65 | MemberLogChannel: os.Getenv("BOT_MEMBER_LOG_CHANNEL"), 66 | }, 67 | &pg.Options{ 68 | Addr: os.Getenv("POSTGRES_ADDR"), 69 | Database: os.Getenv("POSTGRES_DB"), 70 | Password: os.Getenv("POSTGRES_PASSWORD"), 71 | User: os.Getenv("POSTGRES_USER"), 72 | PoolSize: getenvInt("POSTGRES_POOL_SIZE"), 73 | }, 74 | &redis.Options{ 75 | Addr: os.Getenv("REDIS_ADDR"), 76 | Password: os.Getenv("REDIS_PASSWORD"), 77 | PoolSize: getenvInt("REDIS_POOL_SIZE"), 78 | }, 79 | )) 80 | } 81 | --------------------------------------------------------------------------------