diff --git a/discord.go b/discord.go index 0d34535..ef5bf28 100644 --- a/discord.go +++ b/discord.go @@ -39,6 +39,12 @@ var ErrMFA = errors.New("account has 2FA enabled") // With an email, password and auth token - Discord will verify the auth // token, if it is invalid it will sign in with the provided // credentials. This is the Discord recommended way to sign in. +// +// NOTE: While email/pass authentication is supported by DiscordGo it is +// HIGHLY DISCOURAGED by Discord. Please only use email/pass to obtain a token +// and then use that authentication token for all future connections. +// Also, doing any form of automation with a user (non Bot) account may result +// in that account being permanently banned from Discord. func New(args ...interface{}) (s *Session, err error) { // Create an empty Session interface. diff --git a/message.go b/message.go index d3523ab..13c2da0 100644 --- a/message.go +++ b/message.go @@ -11,6 +11,7 @@ package discordgo import ( "fmt" + "io" "regexp" ) @@ -31,11 +32,18 @@ type Message struct { Reactions []*MessageReactions `json:"reactions"` } +// File stores info about files you e.g. send in messages. +type File struct { + Name string + Reader io.Reader +} + // MessageSend stores all parameters you can send with ChannelMessageSendComplex. type MessageSend struct { Content string `json:"content,omitempty"` Embed *MessageEmbed `json:"embed,omitempty"` Tts bool `json:"tts"` + File *File `json:"file"` } // MessageEdit is used to chain parameters via ChannelMessageEditComplex, which diff --git a/restapi.go b/restapi.go index ca53eac..7c9fd81 100644 --- a/restapi.go +++ b/restapi.go @@ -173,6 +173,12 @@ func unmarshal(data []byte, v interface{}) error { // ------------------------------------------------------------------------------------------------ // Login asks the Discord server for an authentication token. +// +// NOTE: While email/pass authentication is supported by DiscordGo it is +// HIGHLY DISCOURAGED by Discord. Please only use email/pass to obtain a token +// and then use that authentication token for all future connections. +// Also, doing any form of automation with a user (non Bot) account may result +// in that account being permanently banned from Discord. func (s *Session) Login(email, password string) (err error) { data := struct { @@ -663,11 +669,28 @@ func (s *Session) GuildBans(guildID string) (st []*GuildBan, err error) { // userID : The ID of a User // days : The number of days of previous comments to delete. func (s *Session) GuildBanCreate(guildID, userID string, days int) (err error) { + return s.GuildBanCreateWithReason(guildID, userID, "", days) +} + +// GuildBanCreateWithReason bans the given user from the given guild also providing a reaso. +// guildID : The ID of a Guild. +// userID : The ID of a User +// reason : The reason for this ban +// days : The number of days of previous comments to delete. +func (s *Session) GuildBanCreateWithReason(guildID, userID, reason string, days int) (err error) { uri := EndpointGuildBan(guildID, userID) + queryParams := url.Values{} if days > 0 { - uri = fmt.Sprintf("%s?delete-message-days=%d", uri, days) + queryParams.Set("delete-message-days", strconv.Itoa(days)) + } + if reason != "" { + queryParams.Set("reason", reason) + } + + if len(queryParams) > 0 { + uri += "?" + queryParams.Encode() } _, err = s.RequestWithBucketID("PUT", uri, nil, EndpointGuildBan(guildID, "")) @@ -1294,7 +1317,59 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) data.Embed.Type = "rich" } - response, err := s.RequestWithBucketID("POST", EndpointChannelMessages(channelID), data, EndpointChannelMessages(channelID)) + endpoint := EndpointChannelMessages(channelID) + + var response []byte + if data.File != nil { + body := &bytes.Buffer{} + bodywriter := multipart.NewWriter(body) + + // What's a better way of doing this? Reflect? Generator? I'm open to suggestions + + if data.Content != "" { + if err = bodywriter.WriteField("content", data.Content); err != nil { + return + } + } + + if data.Embed != nil { + var embed []byte + embed, err = json.Marshal(data.Embed) + if err != nil { + return + } + err = bodywriter.WriteField("embed", string(embed)) + if err != nil { + return + } + } + + if data.Tts { + if err = bodywriter.WriteField("tts", "true"); err != nil { + return + } + } + + var writer io.Writer + writer, err = bodywriter.CreateFormFile("file", data.File.Name) + if err != nil { + return + } + + _, err = io.Copy(writer, data.File.Reader) + if err != nil { + return + } + + err = bodywriter.Close() + if err != nil { + return + } + + response, err = s.request("POST", endpoint, bodywriter.FormDataContentType(), body.Bytes(), endpoint, 0) + } else { + response, err = s.RequestWithBucketID("POST", endpoint, data, endpoint) + } if err != nil { return } @@ -1427,48 +1502,18 @@ func (s *Session) ChannelMessagesPinned(channelID string) (st []*Message, err er // channelID : The ID of a Channel. // name: The name of the file. // io.Reader : A reader for the file contents. -func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (st *Message, err error) { - return s.ChannelFileSendWithMessage(channelID, "", name, r) +func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{File: &File{Name: name, Reader: r}}) } // ChannelFileSendWithMessage sends a file to the given channel with an message. +// DEPRECATED. Use ChannelMessageSendComplex instead. // channelID : The ID of a Channel. // content: Optional Message content. // name: The name of the file. // io.Reader : A reader for the file contents. -func (s *Session) ChannelFileSendWithMessage(channelID, content string, name string, r io.Reader) (st *Message, err error) { - - body := &bytes.Buffer{} - bodywriter := multipart.NewWriter(body) - - if len(content) != 0 { - if err := bodywriter.WriteField("content", content); err != nil { - return nil, err - } - } - - writer, err := bodywriter.CreateFormFile("file", name) - if err != nil { - return nil, err - } - - _, err = io.Copy(writer, r) - if err != nil { - return - } - - err = bodywriter.Close() - if err != nil { - return - } - - response, err := s.request("POST", EndpointChannelMessages(channelID), bodywriter.FormDataContentType(), body.Bytes(), EndpointChannelMessages(channelID), 0) - if err != nil { - return - } - - err = unmarshal(response, &st) - return +func (s *Session) ChannelFileSendWithMessage(channelID, content string, name string, r io.Reader) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{File: &File{Name: name, Reader: r}, Content: content}) } // ChannelInvites returns an array of Invite structures for the given channel diff --git a/state.go b/state.go index d036b5b..c2c5519 100644 --- a/state.go +++ b/state.go @@ -34,6 +34,7 @@ type State struct { TrackMembers bool TrackRoles bool TrackVoice bool + TrackPresences bool guildMap map[string]*Guild channelMap map[string]*Channel @@ -46,13 +47,14 @@ func NewState() *State { PrivateChannels: []*Channel{}, Guilds: []*Guild{}, }, - TrackChannels: true, - TrackEmojis: true, - TrackMembers: true, - TrackRoles: true, - TrackVoice: true, - guildMap: make(map[string]*Guild), - channelMap: make(map[string]*Channel), + TrackChannels: true, + TrackEmojis: true, + TrackMembers: true, + TrackRoles: true, + TrackVoice: true, + TrackPresences: true, + guildMap: make(map[string]*Guild), + channelMap: make(map[string]*Channel), } } @@ -147,6 +149,107 @@ func (s *State) Guild(guildID string) (*Guild, error) { return nil, errors.New("guild not found") } +// PresenceAdd adds a presence to the current world state, or +// updates it if it already exists. +func (s *State) PresenceAdd(guildID string, presence *Presence) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, p := range guild.Presences { + if p.User.ID == presence.User.ID { + //guild.Presences[i] = presence + + //Update status + guild.Presences[i].Game = presence.Game + guild.Presences[i].Roles = presence.Roles + if presence.Status != "" { + guild.Presences[i].Status = presence.Status + } + if presence.Nick != "" { + guild.Presences[i].Nick = presence.Nick + } + + //Update the optionally sent user information + //ID Is a mandatory field so you should not need to check if it is empty + guild.Presences[i].User.ID = presence.User.ID + + if presence.User.Avatar != "" { + guild.Presences[i].User.Avatar = presence.User.Avatar + } + if presence.User.Discriminator != "" { + guild.Presences[i].User.Discriminator = presence.User.Discriminator + } + if presence.User.Email != "" { + guild.Presences[i].User.Email = presence.User.Email + } + if presence.User.Token != "" { + guild.Presences[i].User.Token = presence.User.Token + } + if presence.User.Username != "" { + guild.Presences[i].User.Username = presence.User.Username + } + + return nil + } + } + + guild.Presences = append(guild.Presences, presence) + return nil +} + +// PresenceRemove removes a presence from the current world state. +func (s *State) PresenceRemove(guildID string, presence *Presence) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, p := range guild.Presences { + if p.User.ID == presence.User.ID { + guild.Presences = append(guild.Presences[:i], guild.Presences[i+1:]...) + return nil + } + } + + return errors.New("presence not found") +} + +// Presence gets a presence by ID from a guild. +func (s *State) Presence(guildID, userID string) (*Presence, error) { + if s == nil { + return nil, ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return nil, err + } + + for _, p := range guild.Presences { + if p.User.ID == userID { + return p, nil + } + } + + return nil, errors.New("presence not found") +} + // TODO: Consider moving Guild state update methods onto *Guild. // MemberAdd adds a member to the current world state, or @@ -725,6 +828,45 @@ func (s *State) onInterface(se *Session, i interface{}) (err error) { if s.TrackVoice { err = s.voiceStateUpdate(t) } + case *PresenceUpdate: + if s.TrackPresences { + s.PresenceAdd(t.GuildID, &t.Presence) + } + if s.TrackMembers { + if t.Status == StatusOffline { + return + } + + var m *Member + m, err = s.Member(t.GuildID, t.User.ID) + + if err != nil { + // Member not found; this is a user coming online + m = &Member{ + GuildID: t.GuildID, + Nick: t.Nick, + User: t.User, + Roles: t.Roles, + } + + } else { + + if t.Nick != "" { + m.Nick = t.Nick + } + + if t.User.Username != "" { + m.User.Username = t.User.Username + } + + // PresenceUpdates always contain a list of roles, so there's no need to check for an empty list here + m.Roles = t.Roles + + } + + err = s.MemberAdd(m) + } + } return