Skip to content

Commit

Permalink
Add docker support
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikMcClure committed May 12, 2018
1 parent edae92a commit 99368fa
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 51 deletions.
25 changes: 25 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Use builder image to build and do initial config
FROM golang:alpine as builder
ENV GOOS=linux GOARCH=amd64 CGO_ENABLED=0
ADD . /go
RUN rm sweetiebot/override.go
RUN apk add --no-cache git
RUN go get github.com/blackhole12/discordgo
RUN cd src/github.com/blackhole12/discordgo/;git checkout develop
RUN go get github.com/go-sql-driver/mysql
RUN go build -a -installsuffix cgo -o sweetie.out ./sweetie
RUN go build -a -installsuffix cgo -o updater.out ./updater

FROM alpine:latest
RUN apk add --no-cache ca-certificates
COPY --from=builder /go/sweetie.out /sweetie
COPY --from=builder /go/updater.out /updater
ADD selfhost.json /
ADD sweetiebot.sql /
ADD sweetiebot_tz.sql /
ADD web.css /
ADD web.html /
ADD docker_run.sh /
EXPOSE 80
EXPOSE 443
CMD ["./docker_run.sh"]
3 changes: 3 additions & 0 deletions Dockerfile.mariadb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM mariadb:latest
ADD sweetiebot.sql /docker-entrypoint-initdb.d
ADD sweetiebot_tz.sql /docker-entrypoint-initdb.d
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ Some maplists are whitelists of channels or roles. To change them into a blackli
## Error Recovery
Sweetie Bot can function with no database, but most commands will no longer function, and it will be impossible to respond to PMs. While in this state, there will be no errors in the log about failed database operations, because Sweetie Bot simply won't attempt the operations in the first place until she can re-establish a connection. After a database failure is detected, she will attempt to reconnect to the database every 30 seconds. She also has a deadlock detector which sends fake !about commands through the pipeline every 20 seconds - if Sweetie Bot fails to respond for 1 minute and 40 seconds, she will automatically terminate and restart.

## Docker Instance

Sweetiebot can be run in a docker instance. Clone the repo, then edit the example `selfhost.json` file provided in the root directory. Provide a token from [https://discordapp.com/developers/applications/me](https://discordapp.com/developers/applications/me), provide the mysql root password, and put in the ID of your server. Remember to first add the bot to your server before attempting to start it from your [https://discordapp.com/developers/applications/me](application page). Once you've filled out `selfhost.json`, simply run `docker build .` to build a working image of sweetiebot.

If you are using docker compose, follow all the previous steps, but also edit `docker-compose.yaml`. Replace `<YOUR PASSWORD>` with the same mysql root password you used in `selfhost.json`. Then simply run `docker-compose up` and it will build the images. When first booting up, sweetiebot will fail to connect to the database while it's being built - simply wait a minute or two, and the bot will automatically re-establish a connection once it exists. When sweetie exits, the updater will run, and then the container will terminate. Docker-compose has been set to automatically restart it for you, but you can change this and any other options to suit your needs.

******

©2018 Erik McClure
20 changes: 20 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: '3'
services:
web:
build:
context: .
dockerfile: Dockerfile
restart: always
ports:
- "80:80"
- "443:443"
mariadb:
build:
context: .
dockerfile: Dockerfile.mariadb
expose:
- "3306"
restart: always
environment:
- MYSQL_ROOT_PASSWORD=<YOUR PASSWORD>

3 changes: 3 additions & 0 deletions docker_run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
./sweetie
./updater
1 change: 1 addition & 0 deletions selfhost.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"token": "<YOUR TOKEN>", "dbauth": "root:<YOUR PASSWORD>@tcp(mariadb:3306)/sweetiebot?parseTime=true&collation=utf8mb4_general_ci", "mainguildid": "<YOUR SERVER ID>", "maxconfigsize": 1000000, "maxuniqueitems": 25000}
121 changes: 71 additions & 50 deletions sweetiebot/sweetiebot.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var guildfileregex = regexp.MustCompile("^([0-9]+)[.]json$")
const DiscordEpoch uint64 = 1420070400000

// BotVersion stores the current version of sweetiebot
var BotVersion = Version{0, 9, 9, 20}
var BotVersion = Version{0, 9, 9, 21}

const (
MaxPublicLines = 12
Expand All @@ -62,40 +62,41 @@ type deferPair struct {

// SweetieBot is the primary bot object containing the bot state
type SweetieBot struct {
DB *BotDB
DG *DiscordGoSession
Debug bool `json:"debug"`
changelog map[int]string
SelfID DiscordUser
SelfAvatar string
SelfName string
AppID uint64
AppName string
Owner DiscordUser
Token string `json:"token"`
DBAuth string `json:"dbauth"`
MainGuildID DiscordGuild `json:"mainguildid"`
DebugChannels map[DiscordGuild]DiscordChannel `json:"debugchannels"`
quit uint32 // QuitNone means to keep running. QuitNow means to quit immediately. QuitRaid means to wait until no raids have occurred before quitting
Guilds map[DiscordGuild]*GuildInfo
GuildsLock sync.RWMutex
LastMessages sync.Map
MaxConfigSize int `json:"maxconfigsize"`
MaxUniqueItems uint64 `json:"maxuniqueitems"`
StartTime int64
MessageCount uint32 // 32-bit so we can do atomic ops on a 32-bit platform
heartbeat uint32 // perpetually incrementing heartbeat counter to detect deadlock
locknumber uint32
loader func(*GuildInfo) []Module
memberChan chan *GuildInfo
deferChan chan deferPair
Selfhoster *Selfhost
IsUserMode bool `json:"runasuser"` // True if running as a user for some godawful reason
WebSecure bool `json:"websecure"`
WebDomain string `json:"webdomain"`
WebPort string `json:"webport"`
EmptyGuild *GuildInfo // Holds an empty GuildInfo for running server independent commands
UpdateLock AtomicFlag
DB *BotDB
DG *DiscordGoSession
Debug bool `json:"debug"`
changelog map[int]string
SelfID DiscordUser
SelfAvatar string
SelfName string
AppID uint64
AppName string
Owner DiscordUser
Token string `json:"token"`
DBAuth string `json:"dbauth"`
MainGuildID DiscordGuild `json:"mainguildid"`
DebugChannels map[DiscordGuild]DiscordChannel `json:"debugchannels"`
quit uint32 // QuitNone means to keep running. QuitNow means to quit immediately. QuitRaid means to wait until no raids have occurred before quitting
Guilds map[DiscordGuild]*GuildInfo
GuildsLock sync.RWMutex
LastMessages map[DiscordChannel]int64
LastMessageLock sync.RWMutex
MaxConfigSize int `json:"maxconfigsize"`
MaxUniqueItems uint64 `json:"maxuniqueitems"`
StartTime int64
MessageCount uint32 // 32-bit so we can do atomic ops on a 32-bit platform
heartbeat uint32 // perpetually incrementing heartbeat counter to detect deadlock
locknumber uint32
loader func(*GuildInfo) []Module
memberChan chan *GuildInfo
deferChan chan deferPair
Selfhoster *Selfhost
IsUserMode bool `json:"runasuser"` // True if running as a user for some godawful reason
WebSecure bool `json:"websecure"`
WebDomain string `json:"webdomain"`
WebPort string `json:"webport"`
EmptyGuild *GuildInfo // Holds an empty GuildInfo for running server independent commands
UpdateLock AtomicFlag
}

// IsMainGuild returns true if that guild is considered the main (default) guild
Expand Down Expand Up @@ -192,7 +193,9 @@ func (sb *SweetieBot) AttachToGuild(g *discordgo.Guild) {

ch, e := sb.DG.UserChannelCreate(g.OwnerID)
if e == nil {
sb.DB.SetDefaultServer(SBatoi(g.OwnerID), SBatoi(g.ID)) // This ensures no one blows up another server by accident
if sb.DB.Status.Get() {
sb.DB.SetDefaultServer(SBatoi(g.OwnerID), SBatoi(g.ID)) // This ensures no one blows up another server by accident
}
perms, _ := guild.Bot.DG.UserPermissions(sb.SelfID, guild.ID)
warning := ""
if perms&discordgo.PermissionAdministrator != 0 {
Expand Down Expand Up @@ -267,6 +270,7 @@ func (sb *SweetieBot) AttachToGuild(g *discordgo.Guild) {
guild.Config.Modules.Disabled[ModuleID(strings.ToLower(v.Name()))] = true
}
delete(guild.Config.Modules.CommandDisabled, "setup")
delete(guild.Config.Modules.CommandDisabled, "about")
guild.SaveConfig()
}
if sb.IsMainGuild(guild) {
Expand Down Expand Up @@ -340,26 +344,31 @@ func (sb *SweetieBot) ProcessCommand(m *discordgo.Message, info *GuildInfo, t in
// command := strings.ToLower(strings.SplitN(m.Content[1:], " ", 2)[0])
args, indices := ParseArguments(m.Content[1:])
arg := CommandID(strings.ToLower(args[0]))
if info == nil && sb.DB.Status.Get() {
if info == nil {
info = sb.GetDefaultServer(authorid)
}
if info == nil {
_, independent := sb.EmptyGuild.commands[arg]
if !independent && !sb.DB.Status.Get() {
sb.DG.ChannelMessageSend(m.ChannelID, "```\nA temporary database error means I can't process any private message commands right now.```")
return
}
gIDs := sb.DB.GetUserGuilds(authorid)
if !independent && len(gIDs) != 1 {
sb.DG.ChannelMessageSend(m.ChannelID, "```\nCannot determine what server you belong to! Use !defaultserver to set which server I should use when you PM me.```")
return
gIDs := []uint64{}
if _, independent := sb.EmptyGuild.commands[arg]; !independent {
if !sb.DB.Status.Get() {
sb.DG.ChannelMessageSend(m.ChannelID, "```\nA temporary database error means I can't process any private message commands right now.```")
return
}
gIDs = sb.DB.GetUserGuilds(authorid)
if len(gIDs) != 1 {
sb.DG.ChannelMessageSend(m.ChannelID, "```\nCannot determine what server you belong to! Use !defaultserver to set which server I should use when you PM me.```")
return
}
} else if sb.DB.Status.Get() {
gIDs = sb.DB.GetUserGuilds(authorid)
}

if len(gIDs) == 1 {
sb.GuildsLock.RLock()
info = sb.Guilds[NewDiscordGuild(gIDs[0])]
sb.GuildsLock.RUnlock()
}

if info == nil {
info = sb.EmptyGuild
}
Expand Down Expand Up @@ -482,7 +491,9 @@ func (sb *SweetieBot) MessageCreate(s *discordgo.Session, m *discordgo.MessageCr

channelID := DiscordChannel(m.ChannelID)
t := GetTimestamp(m.Message).Unix()
sb.LastMessages.Store(m.ChannelID, t)
sb.LastMessageLock.Lock()
sb.LastMessages[channelID] = t
sb.LastMessageLock.Unlock()

_, private := sb.ChannelIsPrivate(channelID)
var info *GuildInfo
Expand Down Expand Up @@ -627,7 +638,9 @@ func (sb *SweetieBot) GuildMemberRemove(s *discordgo.Session, m *discordgo.Guild
return
}
userID := DiscordUser(m.User.ID)
sb.DB.RemoveMember(userID.Convert(), SBatoi(info.ID))
if sb.DB.CheckStatus() {
sb.DB.RemoveMember(userID.Convert(), SBatoi(info.ID))
}

if info.ID == SilverServerID {
if _, check := sb.Selfhoster.Donors.Load(m.User.ID); check {
Expand Down Expand Up @@ -769,6 +782,9 @@ func (sb *SweetieBot) FindServers(name string, guilds []uint64) []*GuildInfo {

// GetDefaultServer attempts to find the default server for a user
func (sb *SweetieBot) GetDefaultServer(user uint64) *GuildInfo {
if !sb.DB.Status.Get() {
return nil
}
_, _, _, server := sb.DB.GetUser(user)
if server == nil {
return nil
Expand Down Expand Up @@ -857,9 +873,11 @@ func (sb *SweetieBot) idleCheck(info *GuildInfo, guild *discordgo.Guild) {
if ch.GuildID != guild.ID {
break // Don't allow OnIdle to trigger on channels that aren't in this server
}
t, exists := sb.LastMessages.Load(ch.ID)
sb.LastMessageLock.RLock()
t, exists := sb.LastMessages[DiscordChannel(ch.ID)]
sb.LastMessageLock.RUnlock()
if exists {
diff := tm.Sub(time.Unix(t.(int64), 0))
diff := tm.Sub(time.Unix(t, 0))

for _, h := range info.hooks.OnIdle {
if info.ProcessModule(DiscordChannel(ch.ID), h) && diff >= (time.Duration(h.IdlePeriod(info))*time.Second) {
Expand All @@ -878,6 +896,7 @@ func (sb *SweetieBot) idleCheck(info *GuildInfo, guild *discordgo.Guild) {

func (sb *SweetieBot) idleCheckLoop() {
for atomic.LoadUint32(&sb.quit) != QuitNow {
sb.DB.CheckStatus()
sb.GuildsLock.RLock()
infos := make([]*GuildInfo, 0, len(sb.Guilds))
for _, v := range sb.Guilds {
Expand Down Expand Up @@ -996,6 +1015,7 @@ func New(token string, loader func(*GuildInfo) []Module) *SweetieBot {
AppName: "Sweetie Bot",
DebugChannels: make(map[DiscordGuild]DiscordChannel),
Guilds: make(map[DiscordGuild]*GuildInfo),
LastMessages: make(map[DiscordChannel]int64),
MaxConfigSize: 1000000,
MaxUniqueItems: 25000,
StartTime: time.Now().UTC().Unix(),
Expand All @@ -1008,6 +1028,7 @@ func New(token string, loader func(*GuildInfo) []Module) *SweetieBot {
WebDomain: "localhost",
WebPort: ":80",
changelog: map[int]string{
AssembleVersion(0, 9, 9, 21): "- Put lastmessages back on a lock for better performance\n- Fix docker-specific bugs, add docker self-hosting image, because docker is cool now.",
AssembleVersion(0, 9, 9, 20): "- Improve locking situation, begin concentrated effort to find and eliminate deadlocks via stacktraces\n- The bot now yells at you if you try to set your timezone to Etc/GMT±00",
AssembleVersion(0, 9, 9, 19): "- Fix installer and silver permissions handling\n- Removed polls module, replaced with !poll command in the Misc module that analyzes emoji reaction polls instead.",
AssembleVersion(0, 9, 9, 18): "- Fix database cleanup to preserve banned user comments.",
Expand Down
2 changes: 1 addition & 1 deletion usersmodule/UsersModule.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ func (c *setTimeZoneCommand) Process(args []string, msg *discordgo.Message, indi
}
tz = info.Bot.DB.FindTimeZoneOffset("%"+args[0]+"%", offset*60)
}
if strings.Contains(args[0], "GMT") || (len(tz) == 1 && strings.Contains(tz[0], "GMT")) {
if strings.Contains(strings.ToLower(args[0]), "gmt") || (len(tz) == 1 && strings.Contains(strings.ToLower(tz[0]), "gmt")) {
return "```\nStop. Just stop. That's not going to work for daylight savings. You have to provide a timezone LOCATION, like 'America/Los_Angeles'. If you aren't sure what timezone location to use, check what your operating system is set to.```", false, nil
}

Expand Down

0 comments on commit 99368fa

Please sign in to comment.