1// This program is free software: you can redistribute it and/or modify2// it under the terms of the GNU Affero General Public License as3// published by the Free Software Foundation, either version 3 of the4// License, or (at your option) any later version.5//6// This program is distributed in the hope that it will be useful, but7// WITHOUT ANY WARRANTY; without even the implied warranty of8// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU9// Affero General Public License for more details.10//11// You should have received a copy of the GNU Affero General Public12// License along with this program. If not, see <http://www.gnu.org/licenses/>.1314package twitter1516import (17 "fmt"18 "github.com/ChimeraCoder/anaconda"19 "github.com/nmeum/marvin/irc"20 "github.com/nmeum/marvin/modules"21 "html"22 "net/url"23 "strconv"24 "strings"25)2627// Maximum amount of characters allowed in a tweet.28const maxChars = 1402930type Module struct {31 api *anaconda.TwitterApi32 user anaconda.User33 ReadOnly bool `json:"read_only"`34 ConsumerKey string `json:"consumer_key"`35 ConsumerSecret string `json:"consumer_secret"`36 AccessToken string `json:"access_token"`37 AccessTokenSecret string `json:"access_token_secret"`38}3940func Init(moduleSet *modules.ModuleSet) {41 moduleSet.Register(new(Module))42}4344func (m *Module) Name() string {45 return "twitter"46}4748func (m *Module) Help() string {49 return "USAGE: !tweet TEXT || !reply ID @HANDLE TEXT || !directmsg USER TEXT || !retweet ID || !favorite ID || !stat ID"50}5152func (m *Module) Defaults() {53 m.ReadOnly = false54}5556func (m *Module) Load(client *irc.Client) error {57 anaconda.SetConsumerKey(m.ConsumerKey)58 anaconda.SetConsumerSecret(m.ConsumerSecret)5960 m.api = anaconda.NewTwitterApi(m.AccessToken, m.AccessTokenSecret)61 client.CmdHook("privmsg", m.statCmd)6263 if !m.ReadOnly {64 client.CmdHook("privmsg", m.tweetCmd)65 client.CmdHook("privmsg", m.replyCmd)66 client.CmdHook("privmsg", m.retweetCmd)67 client.CmdHook("privmsg", m.favoriteCmd)68 client.CmdHook("privmsg", m.directMsgCmd)69 }7071 values := url.Values{}72 values.Add("skip_status", "true")7374 user, err := m.api.GetSelf(values)75 if err != nil {76 return err77 } else {78 m.user = user79 }8081 values = url.Values{}82 values.Add("replies", "all")83 values.Add("with", "user")8485 go func(c *irc.Client, v url.Values) {86 for {87 m.streamHandler(c, v)88 }89 }(client, values)9091 return nil92}9394func (m *Module) tweet(t string, v url.Values, c *irc.Client, p irc.Message) error {95 _, err := m.api.PostTweet(t, v)96 if err != nil && len(t) > maxChars {97 return c.Write("NOTICE %s :ERROR: Tweet is too long, remove %d characters",98 p.Receiver, len(t)-maxChars)99 } else if err != nil {100 return c.Write("NOTICE %s :ERROR: %s", p.Receiver, err.Error())101 } else {102 return nil103 }104}105106func (m *Module) tweetCmd(client *irc.Client, msg irc.Message) error {107 splited := strings.Fields(msg.Data)108 if len(splited) < 2 || splited[0] != "!tweet" || !client.Connected(msg.Receiver) {109 return nil110 }111112 status := strings.Join(splited[1:], " ")113 return m.tweet(status, url.Values{}, client, msg)114}115116func (m *Module) replyCmd(client *irc.Client, msg irc.Message) error {117 splited := strings.Fields(msg.Data)118 if len(splited) < 3 || splited[0] != "!reply" || !client.Connected(msg.Receiver) {119 return nil120 }121122 status := strings.Join(splited[2:], " ")123 if !strings.Contains(status, "@") {124 return client.Write("NOTICE %s :ERROR: %s",125 msg.Receiver, "A reply must contain an @mention")126 }127128 values := url.Values{}129 values.Add("in_reply_to_status_id", splited[1])130131 return m.tweet(status, values, client, msg)132}133134func (m *Module) retweetCmd(client *irc.Client, msg irc.Message) error {135 splited := strings.Fields(msg.Data)136 if len(splited) < 2 || splited[0] != "!retweet" || !client.Connected(msg.Receiver) {137 return nil138 }139140 id, err := strconv.Atoi(splited[1])141 if err != nil {142 return err143 }144145 if _, err := m.api.Retweet(int64(id), false); err != nil {146 return client.Write("NOTICE %s :ERROR: %s",147 msg.Receiver, err.Error())148 }149150 return nil151}152153func (m *Module) favoriteCmd(client *irc.Client, msg irc.Message) error {154 splited := strings.Fields(msg.Data)155 if len(splited) < 2 || splited[0] != "!favorite" || !client.Connected(msg.Receiver) {156 return nil157 }158159 id, err := strconv.Atoi(splited[1])160 if err != nil {161 return err162 }163164 if _, err := m.api.Favorite(int64(id)); err != nil {165 return client.Write("NOTICE %s :ERROR: %s",166 msg.Receiver, err.Error())167 }168169 return nil170}171172func (m *Module) directMsgCmd(client *irc.Client, msg irc.Message) error {173 splited := strings.Fields(msg.Data)174 if len(splited) < 3 || splited[0] != "!directmsg" || !client.Connected(msg.Receiver) {175 return nil176 }177178 scname := splited[1]179 status := strings.Join(splited[2:], " ")180181 if _, err := m.api.PostDMToScreenName(status, scname); err != nil {182 return client.Write("NOTICE %s :ERROR: %s",183 msg.Receiver, err.Error())184 }185186 return nil187}188189func (m *Module) statCmd(client *irc.Client, msg irc.Message) error {190 splited := strings.Fields(msg.Data)191 if len(splited) < 2 || splited[0] != "!stat" || !client.Connected(msg.Receiver) {192 return nil193 }194195 id, err := strconv.Atoi(splited[1])196 if err != nil {197 return err198 }199200 tweet, err := m.api.GetTweet(int64(id), url.Values{})201 if err != nil {202 return err203 }204205 return client.Write("NOTICE %s :Stats for tweet %d by %s: ↻ %d ★ %d",206 msg.Receiver, tweet.Id, tweet.User.ScreenName, tweet.RetweetCount, tweet.FavoriteCount)207}208209func (m *Module) streamHandler(client *irc.Client, values url.Values) {210 stream := m.api.UserStream(values)211 for {212 select {213 case event, ok := <-stream.C:214 if !ok {215 break216 }217218 if t := m.formatEvent(event); len(t) > 0 {219 m.notify(client, t)220 }221 }222 }223224 stream.Stop()225}226227func (m *Module) formatEvent(event interface{}) string {228 var msg string229 switch t := event.(type) {230 case anaconda.ApiError:231 msg = fmt.Sprintf("Twitter API error %d: %s", t.StatusCode, t.Decoded.Error())232 case anaconda.StatusDeletionNotice:233 msg = fmt.Sprintf("Tweet %d has been deleted", t.Id)234 case anaconda.DirectMessage:235 msg = fmt.Sprintf("Direct message %d by %s sent to %s: %s", t.Id,236 t.SenderScreenName, t.RecipientScreenName, html.UnescapeString(t.Text))237 case anaconda.Tweet:238 if t.RetweetedStatus != nil && t.User.Id != m.user.Id {239 break240 }241242 msg = fmt.Sprintf("Tweet %d by %s: %s", t.Id, t.User.ScreenName,243 html.UnescapeString(t.Text))244 case anaconda.EventTweet:245 if t.Event.Event != "favorite" || t.Source.Id != m.user.Id {246 break247 }248249 text := html.UnescapeString(t.TargetObject.Text)250 msg = fmt.Sprintf("%s favorited tweet %d by %s: %s",251 t.Source.ScreenName, t.TargetObject.Id, t.Target.ScreenName, text)252 }253254 return msg255}256257func (m *Module) notify(client *irc.Client, text string) {258 for _, ch := range client.Channels {259 client.Write("NOTICE %s :%s", ch, text)260 }261}