package main
import (
"flag"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/bwmarrin/discordgo"
"github.com/rudi9719/loggy"
)
var (
startupTime time . Time
setupToken = fmt . Sprintf ( "!setup %+v" , rand . Intn ( 9999 ) + 1000 )
rebootToken = fmt . Sprintf ( "!reboot %+v" , rand . Intn ( 9999 ) + 1000 )
bump = true
config Config
log = loggy . NewLogger ( config . LogOpts )
lastActiveChan string
lastActiveTime time . Time
token string
configFile string
setupMsg string
dg * discordgo . Session
lastPM = make ( map [ string ] time . Time )
quotes = [ ] string { "The hardest choices require the strongest wills." , "You're strong, but I could snap my fingers and you'd all cease to exist." , "Fun isn't something one considers when balancing the universe. But this... does put a smile on my face." , "Perfectly balanced, as all things should be." , "I am inevitable." }
)
func init ( ) {
flag . StringVar ( & token , "t" , "" , "Bot Token" )
flag . StringVar ( & configFile , "c" , "" , "Config file" )
flag . Parse ( )
}
func main ( ) {
runWeb ( )
defer log . PanicSafe ( )
if configFile == "" {
configFile = "config.json"
} else {
loadConfig ( )
}
log = loggy . NewLogger ( config . LogOpts )
startupTime = time . Now ( )
lastActiveTime = time . Now ( )
lastActiveChan = config . AdminChannel
if token == "" {
log . LogPanic ( "No token provided. Please run: disgord-thanos -t <bot token>" )
}
log . LogCritical ( "SetupToken: %+v\nRebootToken: %+v" , setupToken , rebootToken )
dg , err := discordgo . New ( "Bot " + token )
if err != nil {
log . LogErrorType ( err )
log . LogPanic ( "Unable to create bot using token." )
}
dg . AddHandler ( ready )
dg . AddHandler ( guildMemberRemove )
dg . AddHandler ( guildMemberAdd )
dg . AddHandler ( guildMemberBanned )
dg . AddHandler ( messageCreate )
dg . AddHandler ( readReaction )
dg . AddHandler ( guildMemberUpdate )
dg . Identify . Intents = discordgo . MakeIntent ( discordgo . IntentsAll )
err = dg . Open ( )
if err != nil {
log . LogErrorType ( err )
log . LogPanic ( "Unable to open websocket." )
}
log . LogInfo ( "Thanos is now running. Press CTRL-C to exit." )
go purgeTimer ( dg )
sc := make ( chan os . Signal , 1 )
signal . Notify ( sc , syscall . SIGINT , syscall . SIGTERM , os . Interrupt , os . Kill )
<- sc
saveConfig ( )
dg . Close ( )
}
func exit ( s * discordgo . Session ) {
s . Close ( )
saveConfig ( )
os . Exit ( 0 )
}
func runPurge ( s * discordgo . Session ) {
defer log . PanicSafe ( )
if time . Since ( config . BumpTime ) > 2 * time . Hour {
bump = true
}
for uid , join := range config . Probations {
if time . Since ( join ) > 2 * time . Hour {
delete ( config . Probations , uid )
}
}
for k , v := range config . Unverified {
isUnverified := false
m , err := s . GuildMember ( config . GuildID , k )
if err != nil {
delete ( config . Unverified , k )
continue
}
for _ , role := range m . Roles {
if role == config . MonitorRole {
isUnverified = true
}
}
if isUnverified {
if val , ok := lastPM [ k ] ; ok && time . Since ( val ) < 5 * time . Minute {
continue
}
lastPM [ k ] = time . Now ( )
pmChann , _ := s . UserChannelCreate ( k )
s . ChannelMessageSend ( pmChann . ID ,
fmt . Sprintf ( "This is a reminder that you have not verified with me and will be removed in %+v. You may reply to this message for verification instructions." , time . Until ( v . Add ( 1 * time . Hour ) ) ) )
if time . Since ( v ) > ( time . Hour * 1 ) {
s . ChannelMessageSend ( config . AdminChannel , fmt . Sprintf ( "%+v was removed." , m . Mention ( ) ) )
s . GuildMemberDeleteWithReason ( config . GuildID , k , fmt . Sprintf ( "Unverified user %+v." , v ) )
}
} else {
delete ( config . Unverified , k )
}
}
messages , _ := s . ChannelMessages ( config . MonitorChann , 100 , "" , "" , "" )
for _ , message := range messages {
found := false
for user := range config . Unverified {
if message . Author . ID == user {
found = true
}
for _ , mention := range message . Mentions {
if mention . ID == user {
found = true
}
}
}
if ! found {
s . ChannelMessageDelete ( config . MonitorChann , message . ID )
}
}
saveConfig ( )
}
func ready ( s * discordgo . Session , event * discordgo . Ready ) {
// Set the playing status.
s . UpdateStatus ( 0 , "DreamDaddy v1.0" )
}
func guildMemberUpdate ( s * discordgo . Session , m * discordgo . GuildMemberUpdate ) {
defer log . PanicSafe ( )
for role := range m . Roles {
if fmt . Sprintf ( "%+v" , role ) == config . MonitorRole {
s . ChannelMessageSend ( config . AdminChannel , "New unverified user detected." )
s . ChannelMessageSend ( config . MonitorChann , fmt . Sprintf ( "Welcome %+v, you may PM me your verification, or I will ban you in an hour!\nSay \"!rules\" in this channel, without quotes for the rules. You may private/direct message me for verification instructions.\n\nYou will not be able to read/see other channels or users until you verify." , m . User . Mention ( ) ) )
config . Unverified [ m . User . ID ] = time . Now ( )
config . Probations [ m . User . ID ] = time . Now ( )
saveConfig ( )
}
}
}
func guildMemberAdd ( s * discordgo . Session , m * discordgo . GuildMemberAdd ) {
defer log . PanicSafe ( )
config . Unverified [ m . User . ID ] = time . Now ( )
config . Probations [ m . User . ID ] = time . Now ( )
s . GuildMemberRoleAdd ( config . GuildID , m . User . ID , config . MonitorRole )
s . ChannelMessageSend ( config . MonitorChann , fmt . Sprintf ( "Welcome %+v, you may PM me your verification, or I will ban you in an hour!\nSay \"!rules\" in this channel, without quotes for the rules. You may private/direct message me for verification instructions.\n\nYou will not be able to read/see other channels or users until you verify." , m . User . Mention ( ) ) )
saveConfig ( )
}
func guildMemberBanned ( s * discordgo . Session , m * discordgo . GuildBanAdd ) {
defer log . PanicSafe ( )
for uid := range config . Probations {
if m . User . Email == uid {
delete ( config . Probations , uid )
}
}
saveConfig ( )
}
func guildMemberRemove ( s * discordgo . Session , m * discordgo . GuildMemberRemove ) {
defer log . PanicSafe ( )
go runPurge ( s )
banned := false
for uid , join := range config . Probations {
if time . Since ( join ) < 2 * time . Hour {
if m . User . ID == uid {
banned = true
s . GuildBanCreateWithReason ( config . GuildID , m . User . ID , fmt . Sprintf ( "Left within 2 hours of joining. %+v" , time . Since ( join ) ) , 0 )
delete ( config . Probations , uid )
}
} else {
delete ( config . Probations , uid )
}
}
delete ( config . Unverified , m . User . ID )
s . ChannelMessageSend ( config . AdminChannel , fmt . Sprintf ( "%+v (@%+v) has left, ban: %+v" , m . User . ID , m . User . Username , banned ) )
saveConfig ( )
}
func verifyMember ( s * discordgo . Session , u discordgo . User ) {
defer log . PanicSafe ( )
s . GuildMemberRoleAdd ( config . GuildID , u . ID , config . VerifiedRole )
s . GuildMemberRoleRemove ( config . GuildID , u . ID , config . MonitorRole )
st , _ := s . UserChannelCreate ( u . ID )
s . ChannelMessageSend ( st . ID , "Your verification has been accepted, welcome!" )
s . ChannelMessageSend ( config . IntroChann , fmt . Sprintf ( "Welcome %+v please introduce yourself! :) feel free to check out <#710557387937022034> to tag your roles. Also please mute any channels you are not interested in!" , u . Mention ( ) ) )
}
func rejectVerification ( s * discordgo . Session , u discordgo . User ) {
defer log . PanicSafe ( )
st , _ := s . UserChannelCreate ( u . ID )
if st != nil {
s . ChannelMessageSend ( st . ID , fmt . Sprintf ( "Your verification has been rejected. This means it did not clearly show your face, with your pinkie finger held to the corner of your mouth, or the photo looked edited/filtered. No filters will be accepted.\n\nPlease try again before %+v" , time . Until ( time . Now ( ) . Add ( 1 * time . Hour ) ) ) )
}
config . Unverified [ u . ID ] = time . Now ( )
}
func requestAge ( s * discordgo . Session , u discordgo . User ) {
defer log . PanicSafe ( )
st , _ := s . UserChannelCreate ( u . ID )
s . ChannelMessageSend ( st . ID , "What is your ASL? (Age/Sex/Language)" )
}
func handlePM ( s * discordgo . Session , m * discordgo . MessageCreate ) {
defer log . PanicSafe ( )
if strings . Contains ( m . Content , "Rule" ) || strings . Contains ( m . Content , "rule" ) {
s . ChannelMessageSend ( m . ChannelID , "I specifically said to say \"!rules\" without quotes in the unverified channel for the rules." )
}
for _ , uid := range config . Verifications {
user := userFromID ( s , uid . UserID )
if m . Author . ID == user . ID {
s . ChannelMessageSend ( m . ChannelID , "Your verification is pending. An admin will respond to it when they are available." )
s . ChannelMessageSend ( config . AdminChannel , fmt . Sprintf ( "%+v said: %+v" , m . Author . Mention ( ) , m . Content ) )
return
}
}
if len ( m . Attachments ) != 1 {
s . ChannelMessageSend ( m . ChannelID , "```I am a bot and this is an autoreply.\n\nUntil you send a verification, I will always say the following message:```\nYou may only send me your verification (and nothing else) to be passed to the admins (and no one else). Verification is a clear full face pic, with your pinky finger held to the corner of your mouth." )
s . ChannelMessageSend ( config . AdminChannel , fmt . Sprintf ( "%+v said: %+v" , m . Author . Mention ( ) , m . Content ) )
return
}
delete ( config . Unverified , m . Author . ID )
var v Verification
v . Submitted = time . Now ( )
v . UserID = m . Author . ID
v . Username = m . Author . Username
v . Photo = m . Attachments [ 0 ] . ProxyURL
v . Status = "Submitted"
msg , _ := s . ChannelMessageSend ( config . AdminChannel , fmt . Sprintf ( "%+v\n%+v" , v . Username , v . Photo ) )
config . Verifications [ msg . ID ] = v
s . MessageReactionAdd ( config . AdminChannel , msg . ID , "👎" )
s . MessageReactionAdd ( config . AdminChannel , msg . ID , "👍" )
s . MessageReactionAdd ( config . AdminChannel , msg . ID , "👶" )
s . MessageReactionAdd ( config . AdminChannel , msg . ID , "⛔" )
}
func readReaction ( s * discordgo . Session , m * discordgo . MessageReactionAdd ) {
defer log . PanicSafe ( )
if m . ChannelID != config . AdminChannel || m . UserID == s . State . User . ID {
return
}
admin , _ := s . GuildMember ( config . GuildID , m . UserID )
adminInteraction ( s , admin . User . ID )
verification , ok := config . Verifications [ m . MessageID ]
if ! ok {
return
}
verification . Admin = admin . User . Username
verification . Closed = time . Now ( )
user := userFromID ( s , verification . UserID )
if user . ID == "" {
s . ChannelMessageSend ( config . AdminChannel , fmt . Sprintf ( "%+v, that user was not found, they might have left." , admin . Mention ( ) ) )
delete ( config . Verifications , m . MessageID )
return
}
if m . Emoji . Name == "👎" {
rejectVerification ( s , user )
verification . Status = "Rejected"
} else if m . Emoji . Name == "👍" {
verifyMember ( s , user )
verification . Status = "Accepted"
go storeVerification ( verification )
} else if m . Emoji . Name == "👶" {
requestAge ( s , user )
log . LogInfo ( "%+v has requested ASL for user %+v." , admin . User . Username , user . Username )
return
} else if m . Emoji . Name == "⛔" {
s . GuildBanCreateWithReason ( config . GuildID , user . ID , fmt . Sprintf ( "Underage or too many failed verifications. %+v" , admin . User . Username ) , 5 )
verification . Status = "Banned"
} else {
return
}
log . LogInfo ( "%+v" , verification . prettyPrint ( ) )
delete ( config . Verifications , m . MessageID )
}
func storeVerification ( v Verification ) {
defer log . PanicSafe ( )
fileURL , _ := url . Parse ( v . Photo )
path := fileURL . Path
segments := strings . Split ( path , "/" )
fileName := segments [ len ( segments ) - 1 ]
file , _ := os . Create ( fmt . Sprintf ( "./verifications/%s-%s-%s" , v . UserID , v . Username , fileName ) )
client := http . Client {
CheckRedirect : func ( r * http . Request , via [ ] * http . Request ) error {
r . URL . Opaque = r . URL . Path
return nil
} ,
}
resp , err := client . Get ( v . Photo )
if err != nil {
log . LogError ( "Unable to download verification %s-%s-%s" , v . UserID , v . Username , fileName )
}
defer resp . Body . Close ( )
defer file . Close ( )
_ , err = io . Copy ( file , resp . Body )
if err != nil {
log . LogError ( "Unable to store verification %s-%s-%s" , v . UserID , v . Username , fileName )
}
}
func messageCreate ( s * discordgo . Session , m * discordgo . MessageCreate ) {
defer log . PanicSafe ( )
if m . Author . ID == s . State . User . ID || m . Author . Bot {
return
}
if m . GuildID == "" {
handlePM ( s , m )
return
}
if m . ChannelID == config . MonitorChann {
if strings . Contains ( m . Content , "erif" ) && ! m . Author . Bot {
s . ChannelMessageSend ( m . ChannelID , fmt . Sprintf ( "%+v send me a private message for verification." , m . Author . Mention ( ) ) )
}
return
}
for role := range m . Member . Roles {
if fmt . Sprintf ( "%+v" , role ) == config . AdminRole {
adminInteraction ( s , m . Author . ID )
}
}
if m . ChannelID != config . AdminChannel {
lastActiveChan = m . ChannelID
lastActiveTime = time . Now ( )
}
if strings . HasPrefix ( m . Content , "!d bump" ) {
if time . Since ( config . BumpTime ) < 2 * time . Hour {
s . ChannelMessageSend ( m . ChannelID , fmt . Sprintf ( "Sorry, <@%+v> already claimed the bump. Better luck next time!" , config . LastBumper ) )
return
}
config . LastBumper = m . Author . ID
go bumpTimer ( s )
return
}
if time . Since ( config . BumpTime ) > 2 * time . Hour {
s . ChannelMessageSend ( m . ChannelID , fmt . Sprintf ( "%+v please say \"!d bump\" without the quotes to bump our server :)" , m . Author . Mention ( ) ) )
}
if m . ChannelID == config . AdminChannel {
if strings . HasPrefix ( m . Content , rebootToken ) {
exit ( s )
}
if strings . HasPrefix ( m . Content , "!quote" ) {
quotes = append ( quotes , strings . ReplaceAll ( m . Content , "!quote" , "" ) )
}
if strings . HasPrefix ( m . Content , "!snap" ) || strings . HasPrefix ( m . Content , "!purge" ) {
go runPurge ( s )
s . ChannelMessageSend ( config . AdminChannel , quotes [ rand . Intn ( len ( quotes ) ) ] )
}
if strings . HasPrefix ( m . Content , "!st" ) {
go status ( s )
saveConfig ( )
}
}
}