├── .gitignore ├── DiscordState ├── session.go ├── state.go └── struct.go ├── README.md ├── TODO ├── commands.go ├── config.go ├── events.go ├── getdeps.rc ├── helper.go ├── main.go ├── menu.go └── mkfile /.gitignore: -------------------------------------------------------------------------------- 1 | *.patch 2 | *.diff 3 | disco 4 | -------------------------------------------------------------------------------- /DiscordState/session.go: -------------------------------------------------------------------------------- 1 | //Package DiscordState is an abstraction layer that gives proper structs and functions to get and set the current state of the cli server 2 | package DiscordState 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/bwmarrin/discordgo" 8 | ) 9 | 10 | //!----- Session -----!// 11 | 12 | //NewSession Creates a new Session 13 | func NewSession(Username, Password string) *Session { 14 | Session := new(Session) 15 | Session.Username = Username 16 | Session.Password = Password 17 | 18 | return Session 19 | } 20 | 21 | //Start attaches a discordgo listener to the Sessions and fills it. 22 | func (Session *Session) Start() error { 23 | 24 | fmt.Printf("Connecting...") 25 | 26 | dg, err := discordgo.New(Session.Username, Session.Password) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | // Open the websocket and begin listening. 32 | dg.Open() 33 | 34 | //Retrieve GuildID's from current User 35 | //need index of Guilds[] rather than UserGuilds[] (maybe) 36 | Guilds, err := dg.UserGuilds(0, "", "") 37 | if err != nil { 38 | return err 39 | } 40 | 41 | Session.Guilds = Guilds 42 | 43 | Session.DiscordGo = dg 44 | 45 | Session.User, _ = Session.DiscordGo.User("@me") 46 | 47 | fmt.Printf(" PASSED!\n") 48 | 49 | return nil 50 | } 51 | 52 | //NewState (constructor) attaches a new state to the Guild inside a Session, and fills it. 53 | func (Session *Session) NewState(GuildID string, MessageAmount int) (*State, error) { 54 | State := new(State) 55 | 56 | //Disable Event Handling 57 | State.Enabled = false 58 | 59 | //Set Session 60 | State.Session = Session 61 | 62 | //Set Guild 63 | for _, guildID := range Session.Guilds { 64 | if guildID.ID == GuildID { 65 | Guild, err := State.Session.DiscordGo.Guild(guildID.ID) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | State.Guild = Guild 71 | } 72 | } 73 | 74 | //Retrieve Members 75 | 76 | State.Members = make(map[string]*discordgo.Member) 77 | 78 | for _, Member := range State.Guild.Members { 79 | State.Members[Member.User.Username] = Member 80 | } 81 | 82 | //RetrieveMemberRoles 83 | State.MemberRole = make(map[string]*discordgo.Role) 84 | 85 | for _, Member := range State.Guild.Members { 86 | var MemberRole string 87 | 88 | if len(Member.Roles) > 0 { 89 | MemberRole = Member.Roles[0] 90 | } else { 91 | break 92 | } 93 | 94 | for _, Role := range State.Guild.Roles { 95 | if Role.ID == MemberRole { 96 | State.MemberRole[Member.User.Username] = Role 97 | break 98 | } 99 | } 100 | } 101 | 102 | //Set MessageAmount 103 | State.MessageAmount = MessageAmount 104 | 105 | //Init Messages 106 | State.Messages = []*discordgo.Message{} 107 | 108 | //Retrieve Channels 109 | 110 | State.Channels = State.Guild.Channels 111 | 112 | //Set User Channels 113 | //State.Chan = Session.DiscordGo.UserChannels() 114 | 115 | return State, nil 116 | } 117 | 118 | //Update updates the session, this reloads the Guild list 119 | func (Session *Session) Update() error { 120 | UserGuilds, err := Session.DiscordGo.UserGuilds(0, "", "") 121 | if err != nil { 122 | return err 123 | } 124 | 125 | // this doesn't seem to actually work. 126 | Session.Guilds = UserGuilds 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /DiscordState/state.go: -------------------------------------------------------------------------------- 1 | package DiscordState 2 | 3 | import "github.com/bwmarrin/discordgo" 4 | 5 | //SetChannel sets the channel of the current State 6 | func (State *State) SetChannel(ID string) { 7 | for _, Channel := range State.Channels { 8 | if Channel.ID == ID { 9 | State.Channel = Channel 10 | } 11 | } 12 | } 13 | 14 | //AddMember adds Member to State 15 | func (State *State) AddMember(Member *discordgo.Member) { 16 | State.Members[Member.User.ID] = Member 17 | } 18 | 19 | //DelMember deletes Member from State 20 | func (State *State) DelMember(Member *discordgo.Member) { 21 | delete(State.Members, Member.User.ID) 22 | } 23 | 24 | //AddMessage adds Message to State 25 | func (State *State) AddMessage(Message *discordgo.Message) { 26 | //Do not add if Amount <= 0 27 | if State.MessageAmount <= 0 { 28 | return 29 | } 30 | 31 | //Remove First Message if next message is going to increase length past MessageAmount 32 | if len(State.Messages) == State.MessageAmount { 33 | State.Messages = append(State.Messages[:0], State.Messages[1:]...) 34 | } 35 | 36 | State.Messages = append(State.Messages, Message) 37 | } 38 | 39 | //EditMessage edits Message inside State 40 | func (State *State) EditMessage(Message *discordgo.Message) { 41 | for Index, StateMessage := range State.Messages { 42 | if StateMessage.ID == Message.ID { 43 | State.Messages[Index] = Message 44 | } 45 | } 46 | } 47 | 48 | //DelMessage deletes Message from State 49 | func (State *State) DelMessage(Message *discordgo.Message) { 50 | for Index, StateMessage := range State.Messages { 51 | if StateMessage.ID == Message.ID { 52 | State.Messages = append(State.Messages[:Index], State.Messages[Index+1:]...) 53 | } 54 | } 55 | } 56 | 57 | //RetrieveMessages retrieves last N Messages and puts it in state 58 | func (State *State) RetrieveMessages(Amount int) error { 59 | // See: https://godoc.org/github.com/bwmarrin/discordgo#Channel and https://godoc.org/github.com/bwmarrin/discordgo#State for max message count 60 | Messages, err := State.Session.DiscordGo.ChannelMessages(State.Channel.ID, Amount, "", "", "") 61 | if err != nil { 62 | return err 63 | } 64 | 65 | //Reverse insert Messages 66 | for i := 0; i < len(Messages); i++ { 67 | State.AddMessage(Messages[len(Messages)-i-1]) 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /DiscordState/struct.go: -------------------------------------------------------------------------------- 1 | package DiscordState 2 | 3 | import "github.com/bwmarrin/discordgo" 4 | 5 | //State is the current state of the attached client 6 | type State struct { 7 | Guild *discordgo.Guild 8 | Channel *discordgo.Channel 9 | Channels []*discordgo.Channel 10 | UserChannels []*discordgo.Channel 11 | Members map[string]*discordgo.Member 12 | MemberRole map[string]*discordgo.Role 13 | Messages []*discordgo.Message 14 | Session *Session 15 | MessageAmount int //Amount of Messages to keep in State 16 | Enabled bool //Toggles State for Event handling 17 | } 18 | 19 | //Session contains the 'state' of the attached server 20 | type Session struct { 21 | Username string 22 | User *discordgo.User 23 | Password string 24 | DiscordGo *discordgo.Session 25 | Guilds []*discordgo.UserGuild 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # disco: [Discord](https://discord.gg) client for 9front 2 | 3 | Fork of theboxmage's [discord-cli](https://github.com/theboxmage/discordcli). 4 | 5 | Ndb config is in `$home/lib/disco.ndb` for setting password, should be made automatically after first run. Alternatively, you may use factotum. 6 | 7 | ## Install 8 | 9 | ### Dependencies 10 | 11 | - 9fans.net/go/plan9 12 | - github.com/Plan9-Archive/libauth 13 | - github.com/mischief/ndb 14 | - golang.org/x/crypto 15 | - github.com/gorilla/websocket 16 | - github.com/bwmarrin/discordgo 17 | 18 | ### Installation 19 | 20 | ``` 21 | % go get github.com/henesy/disco 22 | ``` 23 | 24 | ## Usage 25 | 26 | ``` 27 | % disco -h 28 | Usage of disco: 29 | -n Enable notifications 30 | -t Hide timestamps in channel log 31 | -w string 32 | Dimensions to pass through to statusmsg (default "10,10,260,90") 33 | ``` 34 | 35 | ## Commands 36 | 37 | Commands available in chat: 38 | 39 | | Command | Function | 40 | | ------------- |-------------| 41 | | :q | Quits disco | 42 | | :g | Change listening Guild | 43 | | :c [n ?] | Change listening Channel inside Guild, or list channels | 44 | | :m [n] | Display last [n] messages: ex. `:m 2` displays last two messages | 45 | | :p | Pulls up the private channel menu | 46 | | :n name | Change nickname to `name` | 47 | | :! | Print current server information | 48 | | :? | List the available commands | 49 | 50 | You can regex the last message sent using a format such as: 51 | 52 | s/forsynth/forsyth/ 53 | 54 | ## Config 55 | 56 | A basic `$home/lib/disco.ndb` looks something like: 57 | 58 | ``` 59 | auth=pass 60 | loadbacklog=true 61 | messages=10 62 | promptchar=→ 63 | timestampchar=> 64 | 65 | username=coolperson@mycooldomain.com password=somepassword1 66 | ``` 67 | 68 | Note that the auth= tuple accepts 69 | 70 | auth=factotum 71 | 72 | for authentication using a factotum key and will ignore the password= tuple. 73 | 74 | If used, the factotum key should resemble something to the effect of: 75 | 76 | proto=pass server=discordapp.com service=discord user=youremail@domain.com !password=hunter2 77 | 78 | ## Notes 79 | 80 | If you can connect to a channel and see messages, but yours aren't sending, check to make sure your e-mail address is verified. 81 | 82 | ## FAQ 83 | 84 | Q: What if `go get` doesn't work? 85 | 86 | A: If you want to use `go get` on 9front to install disco and its dependencies (recommended) you should use [driusan's dgit](https://github.com/driusan/dgit) as `git`. Alternatively, on 9front specifically, you can wrap Ori's [git9](https://github.com/oridb/git9). 87 | 88 | Q: What if I can't login because of a captcha error? 89 | 90 | A: You'll need to sign in to Discord via the web app (thus solving a captcha) using a browser with html5/js. I recommend an http proxy such as [this](https://github.com/henesy/http-proxy) in conjunction with a system with such a browser.. 91 | 92 | Q: What if I get an error about signing in from a new location? 93 | 94 | A: Discord has sent you an e-mail with a location confirmation link, click it, no js should be required. 95 | 96 | ## Problems 97 | 98 | * Does not create accounts for you, this still needs to be done in a browser/app 99 | * Does not support 2FA (Discord API explicitly does not allow this) 100 | 101 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Commands: 2 | 3 | :r -- register via Register(email) 4 | 5 | :d [n] file -- dump backlog of last 'n' msgs to a file 'file' 6 | 7 | :m needs fixed 8 | 9 | s/word/words/ eats 'word', s/word/words/g works as expected 10 | 11 | Features: 12 | 13 | * read $home/lib/discord.face for profile picture (or just face?) 14 | 15 | * runtime verify proper permissions -rw------- 0600 for discord.ndb 16 | 17 | * flag for larger backlog loading (similar to :m, which is broken atm) 18 | 19 | * CAPTCHA solution -- http-proxy is a bad one 20 | 21 | * refine :p and its menu 22 | * when leaving a pm, drops back to channel dialogue from last guild (feature?) 23 | 24 | * fix :m not displaying more than loaded backlog 25 | 26 | * fix ctrl+d not closing out cleanly (treat the same as :q) 27 | 28 | * add [b] go back to pm's menu from within pm's rather than the :c menu for last guild 29 | 30 | Bugs: 31 | 32 | * (maybe a Go bug) ­ if you do: `drawterm -G … -c disco` then ctrl+c the terminal (from Linux or so), load maxes and memory allocation spikes on the plan9 server 33 | 34 | * The following error at boot: 35 | 36 | Connecting...2019/07/17 23:39:53 [DG0] wsapi.go:552:onEvent() error unmarshalling READY event, json: cannot unmarshal number into Go struct field ReadState.last_message_id of type string 37 | PASSED! 38 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/bwmarrin/discordgo" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // ParseForCommands parses input for Commands, returns message if no command specified, else return is empty 11 | func ParseForCommands(line string) string { 12 | if len(line) < 2 { 13 | return line 14 | } 15 | 16 | switch line[:2] { 17 | case "s/": 18 | // `s/X/Y/` replace X with Y in the last message sent 19 | r := regexp.MustCompile(`s/([^/]+)/([^/]*)/$`) 20 | n := r.FindStringSubmatchIndex(line) 21 | if len(n) < 1 { 22 | return "" 23 | } 24 | i := 0 25 | var m *discordgo.Message 26 | for i = len(State.Messages) - 1; i >= 0; i-- { 27 | m = State.Messages[i] 28 | if m.ChannelID == State.Channel.ID && m.GuildID == State.Guild.ID && m.Author.ID == Session.User.ID { 29 | break 30 | } 31 | } 32 | if i == 0 || !strings.Contains(m.Content, line[n[2]:n[3]]) { 33 | return "" 34 | } 35 | r, err := regexp.Compile("(" + line[n[2]:n[3]] + ")") 36 | if err != nil { 37 | Msg(ErrorMsg, "%s - invalid regex\n", line) 38 | } 39 | rep := r.ReplaceAllString(m.Content, line[n[4]:n[5]]) 40 | newm := discordgo.NewMessageEdit(m.ChannelID, m.ID) 41 | newm = newm.SetContent(rep) 42 | _, err = State.Session.DiscordGo.ChannelMessageEditComplex(newm) 43 | if err != nil { 44 | Msg(ErrorMsg, "%s\n", err) 45 | return "" 46 | } 47 | Msg(TextMsg, "%s → %s\n", m.Content, rep) 48 | return "" 49 | 50 | case ":?": 51 | // Show help menu 52 | Msg(TextMsg, "Commands: \n") 53 | Msg(TextMsg, "s/X/Y/ - Replace X with Y in the last message\n") 54 | Msg(TextMsg, "[:g] - Open guild menu\n") 55 | Msg(TextMsg, "[:p] - Open private message menu\n") 56 | Msg(TextMsg, "[:c] - Open guild channel menu\n") 57 | Msg(TextMsg, "[:c ?] - List guild channels\n") 58 | Msg(TextMsg, "[:c ] - Go directly to channel \n") 59 | Msg(TextMsg, "[:m ] - Display last messages\n") 60 | Msg(TextMsg, "[:n ] - Change username to \n") 61 | Msg(TextMsg, "[:!] - Print current server information\n") 62 | Msg(TextMsg, "[:?] - Print this menu\n") 63 | return "" 64 | 65 | case ":g": 66 | // Choose a server (guild) 67 | SelectGuild() 68 | return "" 69 | 70 | case ":p": 71 | // Enter DM menu 72 | SelectPrivate() 73 | return "" 74 | 75 | case ":c": 76 | // Choose a channel within the server 77 | opts := strings.Split(line, " ") 78 | if len(opts) == 1 { 79 | SelectChannel() 80 | return "" 81 | } 82 | selectID := 0 83 | if opts[1] == "?" { 84 | for _, channel := range State.Channels { 85 | if channel.Type == 0 { 86 | Msg(TextMsg, "[%d] %s\n", selectID, channel.Name) 87 | selectID++ 88 | } 89 | } 90 | return "" 91 | } 92 | selectMap := make(map[int]*discordgo.Channel) 93 | for _, channel := range State.Channels { 94 | if channel.Type == 0 { 95 | selectMap[selectID] = channel 96 | selectID++ 97 | } 98 | } 99 | selection, err := strconv.Atoi(opts[1]) 100 | if err != nil { 101 | Msg(ErrorMsg, "[:c] Argument Error: %s\n", err) 102 | return "" 103 | } 104 | if len(State.Channels) < selection || selection < 0 { 105 | Msg(ErrorMsg, "[:c] Argument Error: Out of bounds\n") 106 | return "" 107 | } 108 | channel := selectMap[selection] 109 | State.SetChannel(channel.ID) 110 | ShowContent() 111 | return "" 112 | 113 | case ":m": 114 | // Load message backlog -- TODO ­ currently does not retrieve more than n messages 115 | AmountStr := strings.Split(line, " ") 116 | if len(AmountStr) < 2 { 117 | Msg(ErrorMsg, "[:m] No Arguments \n") 118 | return "" 119 | } 120 | 121 | Amount, err := strconv.Atoi(AmountStr[1]) 122 | if err != nil { 123 | Msg(ErrorMsg, "[:m] Argument Error: %s \n", err) 124 | return "" 125 | } 126 | 127 | Msg(InfoMsg, "Printing last %d messages!\n", Amount) 128 | State.RetrieveMessages(Amount) 129 | PrintMessages(Amount) 130 | return "" 131 | 132 | case ":n": 133 | // Change nickname 134 | session := State.Session 135 | user := session.User 136 | oldName := user.Username 137 | newName := strings.TrimPrefix(line, ":n ") 138 | _, err := State.Session.DiscordGo.UserUpdate(user.Email, session.Password, newName, user.Avatar, "") 139 | if err != nil { 140 | Msg(ErrorMsg, "[:n] Argument Error: %s\n", err) 141 | return "" 142 | } 143 | Msg(TextMsg, "%s → %s\n", oldName, newName) 144 | return "" 145 | 146 | case ":!": 147 | // Print current server information 148 | Msg(TextMsg, GuildInfo(State.Guild)) 149 | return "" 150 | } 151 | 152 | return line 153 | } 154 | 155 | //SelectGuild selects a new Guild 156 | func SelectGuild() { 157 | State.Enabled = false 158 | SelectGuildMenu() 159 | // Segfaults would happen here 160 | SelectChannelMenu() 161 | State.Enabled = true 162 | ShowContent() 163 | } 164 | 165 | //AddUserChannel moves a user to a private channel with another user. 166 | func AddUserChannel() { 167 | State.Enabled = false 168 | AddUserChannelMenu() 169 | State.Enabled = true 170 | ShowContent() 171 | } 172 | 173 | //SelectChannel selects a new Channel 174 | func SelectChannel() { 175 | State.Enabled = false 176 | SelectChannelMenu() 177 | State.Enabled = true 178 | ShowContent() 179 | } 180 | 181 | //SelectPrivate a private channel 182 | func SelectPrivate() { 183 | State.Enabled = false 184 | SelectPrivateMenu() 185 | State.Enabled = true 186 | ShowContent() 187 | } 188 | 189 | //SelectDeletePrivate a private channel 190 | func SelectDeletePrivate() { 191 | State.Enabled = false 192 | SelectDeletePrivateMenu() 193 | State.Enabled = true 194 | ShowContent() 195 | } 196 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/Plan9-Archive/libauth" 7 | "github.com/mischief/ndb" 8 | "log" 9 | "os" 10 | "os/user" 11 | sc "strconv" 12 | "strings" 13 | ) 14 | 15 | // Types of auth we can do ­ add handling for new modes to atoam() 16 | type AuthModes int 17 | 18 | const ( 19 | Pass AuthModes = iota 20 | Factotum 21 | Unknown // Placeholder 22 | ) 23 | 24 | //Configuration is a struct that contains all configuration fields 25 | type Configuration struct { 26 | AuthMode AuthModes 27 | Username string 28 | LoadBacklog bool 29 | Messages int 30 | PromptChar string 31 | TimestampChar string 32 | password string 33 | } 34 | 35 | // Config is the global configuration of discord-cli 36 | var Config Configuration 37 | 38 | var ConfigPath string = "/lib/disco.ndb" 39 | 40 | // Convert a string such as "true" into true 41 | func atob(s string) bool { 42 | s = strings.ToLower(s) 43 | 44 | if s == "true" { 45 | return true 46 | } 47 | 48 | return false 49 | } 50 | 51 | // Convert a string such as "factotum" into factotum 52 | func atoam(s string) AuthModes { 53 | s = strings.ToLower(s) 54 | 55 | if s == "pass" { 56 | return Pass 57 | } 58 | 59 | if s == "factotum" { 60 | return Factotum 61 | } 62 | 63 | return Unknown 64 | } 65 | 66 | //GetConfig retrieves configuration file from $home/lib/disco.ndb, if it doesn't exist it calls CreateConfig() 67 | func GetConfig() { 68 | //Get User 69 | Start: 70 | usr, err := user.Current() 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | // Get File 76 | file, err := os.Open(usr.HomeDir + ConfigPath) 77 | 78 | if err != nil { 79 | log.Println("Creating new config file") 80 | CreateConfig() 81 | goto Start 82 | } 83 | 84 | file.Close() 85 | 86 | // Decode File 87 | ndb, err := ndb.Open(usr.HomeDir + ConfigPath) 88 | if err != nil { 89 | log.Fatal("error: Could not ") 90 | } 91 | 92 | Config.Username = ndb.Search("username", "").Search("username") 93 | Config.AuthMode = atoam(ndb.Search("auth", "").Search("auth")) 94 | 95 | if Config.AuthMode == Pass { 96 | Config.password = ndb.Search("username", "").Search("password") 97 | } 98 | 99 | Config.LoadBacklog = atob(ndb.Search("loadbacklog", "").Search("loadbacklog")) 100 | Config.Messages, _ = sc.Atoi(ndb.Search("messages", "").Search("messages")) 101 | Config.PromptChar = ndb.Search("promptchar", "").Search("promptchar") 102 | Config.TimestampChar = ndb.Search("timestampchar", "").Search("timestampchar") 103 | 104 | } 105 | 106 | //CreateConfig creates folder inside $home and makes a new empty configuration file 107 | func CreateConfig() { 108 | // Get User 109 | usr, err := user.Current() 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | 114 | // Set Default values 115 | fmt.Print("Input your email: ") 116 | scan := bufio.NewScanner(os.Stdin) 117 | scan.Scan() 118 | 119 | raw := fmt.Sprintf("auth=pass\nloadbacklog=true\nmessages=10\npromptchar=→\ntimestampchar=>\n\nusername=%s password=\n", scan.Text()) 120 | 121 | // Create File 122 | os.Mkdir(usr.HomeDir+"/lib", 0775) 123 | 124 | file, err := os.Create(usr.HomeDir + ConfigPath) 125 | 126 | if err != nil { 127 | log.Fatalln(err) 128 | } 129 | 130 | file.Chmod(0600) 131 | 132 | // PrintToFile 133 | _, err = file.Write([]byte(raw)) 134 | 135 | if err != nil { 136 | log.Fatalln(err) 137 | } 138 | 139 | file.Close() 140 | } 141 | 142 | // CheckState checks the current state for essential missing information, errors will fail the program 143 | func CheckState() { 144 | //Get User 145 | usr, err := user.Current() 146 | 147 | if err != nil { 148 | log.Fatal(err) 149 | } 150 | 151 | if Config.Username == "" { 152 | log.Fatalln("Error: No Username Specified, please edit " + usr.HomeDir + ConfigPath) 153 | } 154 | 155 | // Check and handle password loading 156 | switch Config.AuthMode { 157 | case Factotum: 158 | // Acquire password from factotum 159 | userPwd, err := libauth.Getuserpasswd("proto=pass service=discord user=%s server=discordapp.com", Config.Username) 160 | 161 | if err != nil { 162 | // Factotum didn't get anything 163 | fmt.Fprintln(os.Stderr, "Warning: No success getting key from factotum, consider disabling it in ndb.") 164 | fmt.Fprintln(os.Stderr, "Libauth gave: ", err) 165 | } else { 166 | Config.password = userPwd.Password 167 | } 168 | 169 | case Unknown: 170 | log.Fatalln("Error: incorrect, or no, authmode specified via auth= config tuple. Consider: auth=(pass factotum).") 171 | 172 | default: 173 | // Password is already loaded in auth=pass mode 174 | } 175 | 176 | if Config.password == "" { 177 | log.Fatalln("Error: No password loaded, cannot auth. Are you missing a factotum key or password= tuple?") 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bwmarrin/discordgo" 7 | ) 8 | 9 | func removeReaction(s *discordgo.Session, r *discordgo.MessageReactionRemove) { 10 | 11 | } 12 | 13 | func newReaction(s *discordgo.Session, m *discordgo.MessageReactionAdd) { 14 | } 15 | 16 | // This function will be called (due to AddHandler above) every time a new 17 | // message is created on any channel that the autenticated user has access to. 18 | func newMessage(s *discordgo.Session, m *discordgo.MessageCreate) { 19 | 20 | //State Messages -- don't notify when we're in the channel 21 | if m.ChannelID == State.Channel.ID && State.Enabled { 22 | State.AddMessage(m.Message) 23 | 24 | Messages := ReceivingMessageParser(m.Message) 25 | 26 | for _, Msg := range Messages { 27 | MessagePrint(string(m.Timestamp), m.Author.Username, Msg) 28 | //log.Printf("> %s > %s\n", UserName(m.Author.Username), Msg) 29 | } 30 | return 31 | } 32 | 33 | //Global Mentions 34 | Mention := "@" + State.Session.User.Username 35 | if strings.Contains(m.ContentWithMentionsReplaced(), Mention) { 36 | go Notify(m.Message) 37 | return 38 | } 39 | DMs, err := Session.DiscordGo.UserChannels() 40 | if err != nil { 41 | // No DMs 42 | return 43 | } 44 | for _, channel := range DMs { 45 | if m.ChannelID == channel.ID { 46 | go Notify(m.Message) 47 | return 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /getdeps.rc: -------------------------------------------------------------------------------- 1 | #!/bin/rc 2 | # Get dependencies for disco when no dgit is present 3 | # This script may not be up to date and exists only as a courtesy 4 | 5 | if(! test -e $GOPATH/src){ 6 | mkdir -p $GOPATH/src 7 | } 8 | 9 | gitdeps = (github.com/mischief/ndb github.com/gorilla/websocket github.com/bwmarrin/discordgo) 10 | 11 | cd $GOPATH/src 12 | 13 | # Handle crypto package manually 14 | 15 | echo Downloading golang.org/x/crypto… 16 | mkdir -p $GOPATH/src/golang.org/x/crypto 17 | cd $GOPATH/src/golang.org/x 18 | 19 | hget https://github.com/golang/crypto/archive/master.zip > $GOPATH/src/golang.org/x/master.zip 20 | unzip -f master.zip 21 | mv crypto-master crypto 22 | rm master.zip 23 | 24 | cd $GOPATH/src 25 | 26 | # Handle 9fans package manually 27 | 28 | echo Downloading 9fans.net/go/plan9… 29 | mkdir -p $GOPATH/src/9fans.net/go/plan9 30 | cd $GOPATH/src/9fans.net/go 31 | 32 | hget https://github.com/9fans/go/archive/master.zip > $GOPATH/src/9fans.net/go/master.zip 33 | unzip -f master.zip 34 | mv go-master plan9 35 | rm master.zip 36 | 37 | cd $GOPATH/src 38 | 39 | # Other deps 40 | 41 | echo Downloading github.com/Plan9-Archive/libauth… 42 | go get github.com/Plan9-Archive/libauth 43 | 44 | cd $GOPATH/src 45 | 46 | for(i in $gitdeps){ 47 | echo 'Downloading '^$i^'…' 48 | mkdir -p $i 49 | cd $i 50 | cd .. 51 | hget 'http://'^$i^'/archive/master.zip' > master.zip 52 | unzip -f master.zip 53 | repo = `{echo $i | awk -F '/' '{print $3}'} 54 | mv $repo^'-master' $repo 55 | rm master.zip 56 | cd $GOPATH/src 57 | } 58 | 59 | echo Done. 60 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/bwmarrin/discordgo" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "runtime" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | //HexColor is a struct gives RGB values 18 | type HexColor struct { 19 | R int 20 | G int 21 | B int 22 | } 23 | 24 | //Msg is a composition of Color.New printf functions 25 | func Msg(MsgType, format string, a ...interface{}) { 26 | fmt.Printf(format, a...) 27 | } 28 | 29 | //Header simply prints a header containing state/session information 30 | func Header() { 31 | Msg(InfoMsg, "Welcome, %s!\n\n", State.Session.User.Username) 32 | switch State.Channel.Type { 33 | case discordgo.ChannelTypeGuildText: 34 | Msg(InfoMsg, "Guild: %s, Channel: %s\n", State.Guild.Name, State.Channel.Name) 35 | case discordgo.ChannelTypeDM: 36 | Msg(InfoMsg, "Channel: %s\n", State.Channel.Recipients[0].Username) 37 | case discordgo.ChannelTypeGroupDM: 38 | var nicklist string 39 | for _, user := range State.Channel.Recipients { 40 | nicklist += user.Username 41 | } 42 | Msg(InfoMsg, "Channel: %s\n", nicklist) 43 | } 44 | } 45 | 46 | //ReceivingMessageParser parses receiving message for mentions, images and MultiLine and returns string array 47 | func ReceivingMessageParser(m *discordgo.Message) []string { 48 | Message := m.ContentWithMentionsReplaced() 49 | 50 | //Parse images 51 | for _, Attachment := range m.Attachments { 52 | Message = Message + " " + Attachment.URL 53 | } 54 | 55 | // MultiLine comment parsing 56 | Messages := strings.Split(Message, "\n") 57 | 58 | return Messages 59 | } 60 | 61 | //PrintMessages prints amount of Messages to CLI 62 | func PrintMessages(Amount int) { 63 | for Key, m := range State.Messages { 64 | name := m.Author.Username 65 | if member, ok := State.Members[m.Author.Username]; ok { 66 | if member.Nick != "" { 67 | name = member.Nick 68 | } 69 | } 70 | if Key >= len(State.Messages)-Amount { 71 | Messages := ReceivingMessageParser(m) 72 | for _, Msg := range Messages { 73 | MessagePrint(string(m.Timestamp), name, Msg) 74 | 75 | } 76 | } 77 | } 78 | } 79 | 80 | //Notify uses Notify-Send from libnotify to send a notification when a mention arrives. 81 | func Notify(m *discordgo.Message) { 82 | if *enableNotify == false { 83 | return 84 | } 85 | var Title string 86 | channel, err := State.Session.DiscordGo.Channel(m.ChannelID) 87 | if err != nil { 88 | Msg(ErrorMsg, "(NOT) PM Error: %s\n", err) 89 | } 90 | switch channel.Type { 91 | case discordgo.ChannelTypeGuildText: 92 | Channel, err := State.Session.DiscordGo.Channel(m.ChannelID) 93 | if err != nil { 94 | Msg(ErrorMsg, "(NOT) Channel Error: %s\n", err) 95 | } 96 | Guild, err := State.Session.DiscordGo.Guild(Channel.GuildID) 97 | if err != nil { 98 | Msg(ErrorMsg, "(NOT) Guild Error: %s\n", err) 99 | } 100 | Title = "@" + m.Author.Username + " : " + Guild.Name + "/" + Channel.Name 101 | case discordgo.ChannelTypeDM: 102 | Title = fmt.Sprintf("%s (pm)\n", m.Author.Username) 103 | } 104 | switch runtime.GOOS { 105 | case "plan9": 106 | pr, pw := io.Pipe() 107 | cmd := exec.Command("/bin/aux/statusmsg", *notifyFlag, Title) 108 | cmd.Stdin = pr 109 | go func() { 110 | defer pw.Close() 111 | fmt.Fprintf(pw, "%s\n", m.ContentWithMentionsReplaced()) 112 | cmd.Wait() 113 | }() 114 | err := cmd.Start() 115 | if err != nil { 116 | Msg(ErrorMsg, "%s\n", err) 117 | } 118 | ioutil.WriteFile("/dev/wctl", []byte("current"), 0644) 119 | default: 120 | cmd := exec.Command("notify-send", Title, m.ContentWithMentionsReplaced()) 121 | err := cmd.Start() 122 | if err != nil { 123 | Msg(ErrorMsg, "(NOT) Check if libnotify is installed, or disable notifications.\n") 124 | } 125 | } 126 | 127 | } 128 | 129 | //MessagePrint prints one correctly formatted Message to stdout 130 | func MessagePrint(Time, Username, Content string) { 131 | //Clean up emoji 132 | content := ParseForEmoji(Content) 133 | //var Color color.Attribute 134 | log.SetFlags(0) 135 | if !*timeStamp { 136 | log.Printf("%s %s %s\n", Username, Config.PromptChar, content) 137 | } else { 138 | TimeStamp, _ := time.Parse(time.RFC3339, Time) 139 | LocalTime := TimeStamp.Local().Format("2006/01/02 15:04:05") 140 | log.Printf("%s %s %s %s %s\n", LocalTime, Config.TimestampChar, Username, Config.PromptChar, content) 141 | 142 | } 143 | log.SetFlags(log.LstdFlags) 144 | } 145 | 146 | func dis(a, b int) float64 { 147 | return float64((a - b) * (a - b)) 148 | } 149 | 150 | // Turn raw mode on -- rio in Plan9 only 151 | func Rawon() (*os.File, error) { 152 | consctl, err := os.OpenFile("/dev/consctl", os.O_WRONLY, 0200) 153 | if err != nil { 154 | /* not on Plan 9 */ 155 | fmt.Println("\nNot running on Plan 9") 156 | return consctl, err 157 | } 158 | 159 | rawon := []byte("rawon") 160 | _, err = consctl.Write(rawon) 161 | if err != nil { 162 | consctl.Close() 163 | return consctl, err 164 | } 165 | 166 | return consctl, nil 167 | } 168 | 169 | // Turn raw mode off -- rio in Plan9 only 170 | func RawOff(consctl *os.File) error { 171 | rawoff := []byte("rawoff") 172 | _, err := consctl.Write(rawoff) 173 | if err != nil { 174 | consctl.Close() 175 | return err 176 | } 177 | 178 | consctl.Close() 179 | return nil 180 | } 181 | 182 | // Return the text currently in /dev/cons -- Plan9 only 183 | func GetCons() string { 184 | cons, err := os.OpenFile("/dev/cons", os.O_RDWR, 0600) 185 | if err != nil { 186 | fmt.Println("Failed to open /dev/cons") 187 | } 188 | consScan := bufio.NewScanner(cons) 189 | consScan.Scan() 190 | return consScan.Text() 191 | } 192 | 193 | // Nicely stringifies the useful info about a guild -- ends in \n 194 | func GuildInfo(g *discordgo.Guild) (s string) { 195 | s += "\n" 196 | s += fmt.Sprintf("Name:\t\t%s\n", g.Name) 197 | s += fmt.Sprintf("ID:\t\t\t%s\n", g.ID) 198 | s += fmt.Sprintf("Icon:\t\t%s\n", g.Icon) 199 | s += fmt.Sprintf("Region:\t\t%s\n", g.Region) 200 | s += fmt.Sprintf("Owner ID:\t%s\n", g.OwnerID) 201 | s += fmt.Sprintf("Join time:\t%s\n", g.JoinedAt) 202 | s += fmt.Sprintf("# Members:\t%d\n", g.MemberCount) 203 | s += fmt.Sprintf("# Channels:\t%d\n", len(g.Channels)) 204 | s += fmt.Sprintf("# Roles:\t%d\n", len(g.Roles)) 205 | s += fmt.Sprintf("# Emojis:\t%d\n", len(g.Emojis)) 206 | s += "\n" 207 | 208 | return 209 | } 210 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // This file provides a basic "quick start" example of using the Discordgo 2 | // package to connect to Discord using the New() helper function. 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "flag" 8 | "fmt" 9 | "github.com/henesy/disco/DiscordState" 10 | "log" 11 | "io" 12 | "os" 13 | "regexp" 14 | "strings" 15 | ) 16 | 17 | // Global Message Types 18 | const ( 19 | ErrorMsg = "Error" 20 | InfoMsg = "Info" 21 | HeaderMsg = "Head" 22 | TextMsg = "Text" 23 | ) 24 | 25 | // Version is current version const 26 | const Version = "2.3" 27 | 28 | // Session is global Session 29 | var Session *DiscordState.Session 30 | 31 | // State is global State 32 | var State *DiscordState.State 33 | 34 | // UserChannels is global User Channels 35 | 36 | // MsgType is a string containing global message type 37 | type MsgType string 38 | 39 | var timeStamp = flag.Bool("t", false, "Hide timestamps in channel log") 40 | var enableNotify = flag.Bool("n", false, "Enable notifications") 41 | var notifyFlag = flag.String("w", "10,10,260,90", "Dimensions to pass through to statusmsg") 42 | 43 | func main() { 44 | 45 | flag.Parse() 46 | if flag.Lookup("h") != nil { 47 | flag.Usage() 48 | os.Exit(1) 49 | } 50 | 51 | if flag.Lookup("w") != nil { 52 | *notifyFlag = fmt.Sprintf("-w %s", *notifyFlag) 53 | } 54 | 55 | // Initialize Config 56 | GetConfig() 57 | CheckState() 58 | Msg(HeaderMsg, "disco version: %s\n\n", Version) 59 | 60 | // NewSession 61 | Session = DiscordState.NewSession(Config.Username, Config.password) //Please don't abuse 62 | err := Session.Start() 63 | if err != nil { 64 | log.Println("Session Failed") 65 | log.Fatalln(err) 66 | } 67 | 68 | // Attach New Window 69 | InitWindow() 70 | 71 | // Attach Event Handlers 72 | State.Session.DiscordGo.AddHandler(newMessage) 73 | //State.Session.DiscordGo.AddHandler(newReaction) 74 | 75 | //defer rl.Close() 76 | log.SetOutput(os.Stderr) // let "log" write to l.Stderr instead of os.Stderr 77 | State.Session.DiscordGo.UpdateStatus(0, "Plan 9") 78 | 79 | // Start Listening 80 | reader := bufio.NewReader(os.Stdin) 81 | for { 82 | //fmt.Print("> ") 83 | //line, _ := rl.Readline() 84 | line, err := reader.ReadString('\n') 85 | 86 | if err != nil { 87 | if err != io.EOF { 88 | fmt.Println("Line read error:", err) 89 | } 90 | break 91 | } 92 | 93 | // ``` multi-line code blocks 94 | if strings.HasPrefix(line, "```") { 95 | for { 96 | subline, _ := reader.ReadString('\n') 97 | line += subline 98 | if strings.Index(subline, "```") != -1 { 99 | break 100 | } 101 | } 102 | _, err := State.Session.DiscordGo.ChannelMessageSend(State.Channel.ID, line) 103 | if err != nil { 104 | fmt.Printf("Error: %s\n", err) 105 | } 106 | continue 107 | } 108 | 109 | n := len(line) 110 | if n > 0 { 111 | line = line[:n-1] 112 | } 113 | 114 | // QUIT 115 | if line == ":q" || line == "" { 116 | break 117 | } 118 | 119 | // Parse Commands 120 | line = ParseForCommands(line) 121 | 122 | line = ParseForMentions(line) 123 | 124 | if line != "" { 125 | _, err := State.Session.DiscordGo.ChannelMessageSend(State.Channel.ID, line) 126 | if err != nil { 127 | fmt.Println("Error:", err) 128 | } 129 | } 130 | } 131 | 132 | return 133 | } 134 | 135 | // InitWindow creates a New CLI Window 136 | func InitWindow() { 137 | SelectGuildMenu() 138 | 139 | if State.Channel == nil { 140 | SelectChannelMenu() 141 | } 142 | 143 | State.Enabled = true 144 | ShowContent() 145 | } 146 | 147 | // ShowContent shows default Channel content 148 | func ShowContent() { 149 | Header() 150 | 151 | if Config.LoadBacklog { 152 | State.RetrieveMessages(Config.Messages) 153 | PrintMessages(Config.Messages) 154 | } 155 | } 156 | 157 | // ShowEmptyContent shows an empty channel 158 | func ShowEmptyContent() { 159 | Header() 160 | } 161 | 162 | // ParseForMentions parses input string for mentions 163 | func ParseForMentions(line string) string { 164 | r, err := regexp.Compile("@\\w+") 165 | 166 | if err != nil { 167 | Msg(ErrorMsg, "Regex Error: %s\n", err) 168 | } 169 | 170 | lineByte := r.ReplaceAllStringFunc(line, ReplaceMentions) 171 | 172 | return lineByte 173 | } 174 | 175 | // ReplaceMentions replaces mentions to ID 176 | func ReplaceMentions(input string) string { 177 | if len(input) < 2 { 178 | return input 179 | } 180 | 181 | name := input[1:] 182 | 183 | // Get up to 1000 members as per documented max starting from the top 184 | members, err := Session.DiscordGo.GuildMembers(State.Guild.ID, "", 1000) 185 | if err != nil { 186 | Msg(ErrorMsg, "Could not Lookup Members: %s\n", err) 187 | return input 188 | } 189 | 190 | // Check for guild members that match 191 | for _, member := range members { 192 | if strings.HasPrefix(member.Nick, name) { 193 | return member.User.Mention() 194 | } 195 | 196 | if strings.HasPrefix(member.User.Username, name) { 197 | return member.User.Mention() 198 | } 199 | } 200 | 201 | // Walk all PM channels 202 | userChannels, err := Session.DiscordGo.UserChannels() 203 | 204 | if err != nil { 205 | return input 206 | } 207 | 208 | for _, channel := range userChannels { 209 | for _, recipient := range channel.Recipients { 210 | if strings.HasPrefix(name, recipient.Username) { 211 | return recipient.Mention() 212 | } 213 | } 214 | } 215 | 216 | return input 217 | } 218 | 219 | // Parse for guild-specific emoji 220 | func ParseForEmoji(line string) string { 221 | r, err := regexp.Compile("<(:\\w+:)[0-9]+>") 222 | 223 | if err != nil { 224 | Msg(ErrorMsg, "Regex Error: %s\n", err) 225 | } 226 | 227 | return r.ReplaceAllString(line, "$1") 228 | } 229 | -------------------------------------------------------------------------------- /menu.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strconv" 9 | ) 10 | 11 | //SelectPrivateMenu is a menu item that changes to a private channel 12 | func SelectPrivateMenu() { 13 | 14 | Start: 15 | 16 | Msg(InfoMsg, "Select a Member:\n") 17 | // List of PMs 18 | UserChannels, err := Session.DiscordGo.UserChannels() 19 | 20 | if err != nil { 21 | Msg(ErrorMsg, "No Private Channels\n") 22 | } 23 | 24 | UserMap := make(map[int]string) 25 | SelectID := 0 26 | 27 | for _, user := range UserChannels { 28 | UserMap[SelectID] = user.ID 29 | // We have to loop through all recipients 30 | recipients := UserChannels[SelectID].Recipients 31 | for _, recipient := range recipients { 32 | if recipient.ID == user.ID { 33 | continue 34 | } 35 | if recipient.Username == "" { 36 | continue 37 | } 38 | Msg(TextMsg, "[%d] %s\n", SelectID, recipient.Username) 39 | break 40 | } 41 | SelectID++ 42 | } 43 | Msg(TextMsg, "[b] Extra Options\n") 44 | var response string 45 | fmt.Scanf("%s\n", &response) 46 | 47 | ResponseInteger, err := strconv.Atoi(response) 48 | 49 | if response == "b" { 50 | New: 51 | Msg(InfoMsg, "Extra Options:\n") 52 | Msg(TextMsg, "[n] Join New User Channel\n") 53 | Msg(TextMsg, "[d] Leave User Channel\n") 54 | Msg(TextMsg, "[b] Go Back\n") 55 | 56 | var response string 57 | fmt.Scanf("%s\n", &response) 58 | 59 | switch response { 60 | case "n": 61 | if State.Channel != nil { 62 | AddUserChannel() 63 | ShowEmptyContent() 64 | goto End 65 | } else { 66 | Msg(ErrorMsg, "Join a guild before attempting to join a user channel\n") 67 | goto New 68 | } 69 | case "d": 70 | SelectDeletePrivate() 71 | goto Start 72 | case "b": 73 | goto Start 74 | default: 75 | goto New 76 | } 77 | } 78 | 79 | if err != nil { 80 | Msg(ErrorMsg, "(GU) Conversion Error: %s\n", err) 81 | goto Start 82 | } 83 | 84 | if ResponseInteger > SelectID-1 || ResponseInteger < 0 { 85 | Msg(ErrorMsg, "(GU) Error: ID is out of bounds\n") 86 | goto Start 87 | } 88 | 89 | State.Channel = UserChannels[ResponseInteger] 90 | End: 91 | } 92 | 93 | //SelectDeletePrivateMenu deletes a user channel 94 | func SelectDeletePrivateMenu() { 95 | 96 | Start: 97 | 98 | Msg(InfoMsg, "Select a Member:\n") 99 | 100 | UserChannels, err := Session.DiscordGo.UserChannels() 101 | 102 | if err != nil { 103 | Msg(ErrorMsg, "No Private Channels\n") 104 | } 105 | 106 | UserMap := make(map[int]string) 107 | SelectID := 0 108 | 109 | for _, user := range UserChannels { 110 | UserMap[SelectID] = user.ID 111 | // We have to loop through all recipients 112 | recipients := UserChannels[SelectID].Recipients 113 | for _, recipient := range recipients { 114 | if recipient.ID == user.ID { 115 | continue 116 | } 117 | if recipient.Username == "" { 118 | continue 119 | } 120 | Msg(TextMsg, "[%d] %s\n", SelectID, recipient.Username) 121 | break 122 | } 123 | SelectID++ 124 | } 125 | var response string 126 | fmt.Scanf("%s\n", &response) 127 | 128 | ResponseInteger, err := strconv.Atoi(response) 129 | if err != nil { 130 | Msg(ErrorMsg, "(GU) Conversion Error: %s\n", err) 131 | goto Start 132 | } 133 | 134 | if ResponseInteger > SelectID-1 || ResponseInteger < 0 { 135 | Msg(ErrorMsg, "(GU) Error: ID is out of bounds\n") 136 | goto Start 137 | } 138 | 139 | Session.DiscordGo.ChannelDelete(UserChannels[ResponseInteger].ID) 140 | 141 | } 142 | 143 | //SelectGuildMenu is a menu item that creates a new State on basis of Guild selection 144 | func SelectGuildMenu() { 145 | var err error 146 | 147 | Start: 148 | 149 | Msg(InfoMsg, "Select a Guild:\n") 150 | 151 | SelectMap := make(map[int]string) 152 | SelectID := 0 153 | 154 | for _, guild := range Session.Guilds { 155 | SelectMap[SelectID] = guild.ID 156 | Msg(TextMsg, "[%d] %s\n", SelectID, guild.Name) 157 | SelectID++ 158 | } 159 | Msg(TextMsg, "[b] Extra Options\n") 160 | Msg(TextMsg, "[p] Private Channels\n") 161 | 162 | var response string 163 | fmt.Scanf("%s\n", &response) 164 | ResponseInteger, err := strconv.Atoi(response) 165 | 166 | if response == "b" { 167 | ExtraGuildMenuOptions() 168 | goto Start 169 | } 170 | 171 | if response == "p" { 172 | if State != nil { 173 | SelectPrivate() 174 | } else { 175 | State, err = Session.NewState(SelectMap[0], Config.Messages) 176 | 177 | if err != nil { 178 | log.Fatal(err) 179 | } 180 | 181 | SelectPrivate() 182 | } 183 | } else { 184 | if err != nil { 185 | Msg(ErrorMsg, "(GU) Conversion Error: %s\n", err) 186 | goto Start 187 | } 188 | 189 | if ResponseInteger > SelectID-1 || ResponseInteger < 0 { 190 | Msg(ErrorMsg, "(GU) Error: ID is out of bounds\n") 191 | goto Start 192 | } 193 | 194 | State, err = Session.NewState(SelectMap[ResponseInteger], Config.Messages) 195 | if err != nil { 196 | log.Fatal(err) 197 | } 198 | } 199 | } 200 | 201 | //SelectChannelMenu is a menu item that sets the current channel 202 | func SelectChannelMenu() { 203 | Start: 204 | Msg(InfoMsg, "Select a Channel:\n") 205 | 206 | SelectMap := make(map[int]string) 207 | SelectID := 0 208 | 209 | for _, channel := range State.Channels { 210 | // 0 is ChannelTypeGuildText ChannelType = iota 211 | if channel.Type == 0 { 212 | SelectMap[SelectID] = channel.ID 213 | Msg(TextMsg, "[%d] %s\n", SelectID, channel.Name) 214 | SelectID++ 215 | } 216 | } 217 | Msg(TextMsg, "[b] Go Back\n") 218 | 219 | var response string 220 | fmt.Scanf("%s\n", &response) 221 | 222 | if response == "b" { 223 | SelectGuildMenu() 224 | goto Start 225 | } 226 | 227 | ResponseInteger, err := strconv.Atoi(response) 228 | if err != nil { 229 | Msg(ErrorMsg, "(CH) Conversion Error: %s\n", err) 230 | goto Start 231 | } 232 | 233 | if ResponseInteger > SelectID-1 || ResponseInteger < 0 { 234 | Msg(ErrorMsg, "(CH) Error: ID is out of bound\n") 235 | goto Start 236 | } 237 | 238 | State.SetChannel(SelectMap[ResponseInteger]) 239 | } 240 | 241 | //ExtraGuildMenuOptions prints and handles extra options for SelectGuildMenu 242 | func ExtraGuildMenuOptions() { 243 | Start: 244 | Msg(InfoMsg, "Extra Options:\n") 245 | Msg(TextMsg, "[n] Join New Server\n") 246 | Msg(TextMsg, "[d] Leave Server\n") 247 | Msg(TextMsg, "[o] Join Official 9fans Server\n") 248 | Msg(TextMsg, "[b] Go Back\n") 249 | 250 | var response string 251 | fmt.Scanf("%s\n", &response) 252 | 253 | switch response { 254 | case "n": 255 | New: 256 | Msg(TextMsg, "Please input invite number ([b] back):\n") 257 | fmt.Scanf("%s\n", &response) 258 | if response == "b" { 259 | goto Start 260 | } 261 | Invite, err := Session.DiscordGo.Invite(response) 262 | if err != nil { 263 | Msg(ErrorMsg, "Invalid Invite\n") 264 | goto New 265 | } 266 | Msg(TextMsg, "Join %s ? [y/n]: ", Invite.Guild.Name) 267 | reader := bufio.NewReader(os.Stdin) 268 | response, _ := reader.ReadString('\n') 269 | response = response[:len(response)-1] 270 | fmt.Println(response, " ", len(response)) 271 | 272 | //fmt.Scanf("%s\n", &response) 273 | if response == "y" { 274 | Session.DiscordGo.InviteAccept(Invite.Code) 275 | err := Session.Update() 276 | if err != nil { 277 | Msg(ErrorMsg, "Session Update Failed: %s\n", err) 278 | } 279 | } else { 280 | goto Start 281 | } 282 | case "o": 283 | _, err := Session.DiscordGo.InviteAccept("https://discord.gg/6daut5T") 284 | if err != nil { 285 | Msg(ErrorMsg, "Joining Official 9fans Server failed\n") 286 | goto Start 287 | } 288 | Msg(InfoMsg, "Joined Official 9fans Server!\n") 289 | case "d": 290 | LeaveServerMenu() 291 | goto Start 292 | default: 293 | return 294 | } 295 | 296 | return 297 | } 298 | 299 | //ExtraPrivateMenuOptions adds functionality to UserChannels. 300 | func ExtraPrivateMenuOptions() { 301 | 302 | return 303 | } 304 | 305 | //AddUserChannelMenu takes a user from the current guild and adds them to a private message. WILL RETURN ERROR IF IN USER CHANNEL. 306 | func AddUserChannelMenu() { 307 | 308 | if State.Channel.GuildID == "" { 309 | Msg(ErrorMsg, "Currently in a user channel, move to a guild with :g\n") 310 | } else { 311 | SelectMap := make(map[int]string) 312 | Start: 313 | SelectID := 0 314 | for _, Member := range State.Members { 315 | SelectMap[SelectID] = Member.User.ID 316 | Msg(TextMsg, "[%d] %s\n", SelectID, Member.User.Username) 317 | SelectID++ 318 | } 319 | var response string 320 | fmt.Scanf("%s\n", &response) 321 | 322 | if response == "b" { 323 | return 324 | } 325 | 326 | ResponseInteger, err := strconv.Atoi(response) 327 | if err != nil { 328 | Msg(ErrorMsg, "(CH) Conversion Error: %s\n", err) 329 | goto Start 330 | } 331 | 332 | if ResponseInteger > SelectID-1 || ResponseInteger < 0 { 333 | Msg(ErrorMsg, "(CH) Error: ID is out of bound\n") 334 | goto Start 335 | } 336 | Chan, err := Session.DiscordGo.UserChannelCreate(SelectMap[ResponseInteger]) 337 | 338 | if Chan.LastMessageID == "" { 339 | var firstMessage string 340 | fmt.Scanf("%s\n", &firstMessage) 341 | Session.DiscordGo.ChannelMessageSend(Chan.ID, "Test") 342 | } 343 | State.Channel = Chan 344 | } 345 | } 346 | 347 | //LeaveServerMenu is a copy of SelectGuildMenu that leaves instead of selects 348 | func LeaveServerMenu() { 349 | var err error 350 | 351 | Start: 352 | 353 | Msg(InfoMsg, "Leave a Guild:\n") 354 | 355 | SelectMap := make(map[int]string) 356 | SelectID := 0 357 | 358 | for _, guild := range Session.Guilds { 359 | SelectMap[SelectID] = guild.ID 360 | Msg(TextMsg, "[%d] %s\n", SelectID, guild.Name) 361 | SelectID++ 362 | } 363 | Msg(TextMsg, "[b] Go Back\n") 364 | 365 | var response string 366 | fmt.Scanf("%s\n", &response) 367 | 368 | if response == "b" { 369 | return 370 | } 371 | 372 | ResponseInteger, err := strconv.Atoi(response) 373 | if err != nil { 374 | Msg(ErrorMsg, "(GUD) Conversion Error: %s\n", err) 375 | goto Start 376 | } 377 | 378 | if ResponseInteger > SelectID-1 || ResponseInteger < 0 { 379 | Msg(ErrorMsg, "(GUD) Error: ID is out of bounds\n") 380 | goto Start 381 | } 382 | 383 | Guild, err := Session.DiscordGo.Guild(SelectMap[ResponseInteger]) 384 | if err != nil { 385 | Msg(ErrorMsg, "(GUD) Unknown Error: %s\n", err) 386 | goto Start 387 | } 388 | 389 | Msg(TextMsg, "Leave %s ? [y/n]:\n", Guild.Name) 390 | fmt.Scanf("%s\n", &response) 391 | if response == "y" { 392 | Session.DiscordGo.GuildLeave(Guild.ID) 393 | err := Session.Update() 394 | if err != nil { 395 | Msg(ErrorMsg, "Session Update Failed: %s\n", err) 396 | } 397 | } else { 398 | goto Start 399 | } 400 | 401 | } 402 | -------------------------------------------------------------------------------- /mkfile: -------------------------------------------------------------------------------- 1 | GOOS=plan9 2 | GOARCH=$objtype 3 | 4 | all: 5 | go build 6 | 7 | install: all 8 | cp ./disco $home/bin/$GOARCH/disco 9 | 10 | bins: 11 | plat=(plan9 linux windows) 12 | arch=(amd64 386 arm) 13 | for(p in $plat){ 14 | for(a in $arch){ 15 | mkdir -p bin/$p/$a 16 | GOOS=$p GOARCH=$a go build 17 | file = disco 18 | if(! test -e $file){ 19 | file = disco.exe 20 | } 21 | cp $file bin/$p/$a/ 22 | rm $file 23 | } 24 | } 25 | --------------------------------------------------------------------------------