1// This program is free software: you can redistribute it and/or modify
2// it under the terms of the GNU Affero General Public License as
3// published by the Free Software Foundation, either version 3 of the
4// License, or (at your option) any later version.
5//
6// This program is distributed in the hope that it will be useful, but
7// WITHOUT ANY WARRANTY; without even the implied warranty of
8// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9// Affero General Public License for more details.
10//
11// You should have received a copy of the GNU Affero General Public
12// License along with this program. If not, see <http://www.gnu.org/licenses/>.
13
14package twitter
15
16import (
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)
26
27// Maximum amount of characters allowed in a tweet.
28const maxChars = 140
29
30type Module struct {
31 api *anaconda.TwitterApi
32 user anaconda.User
33 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}
39
40func Init(moduleSet *modules.ModuleSet) {
41 moduleSet.Register(new(Module))
42}
43
44func (m *Module) Name() string {
45 return "twitter"
46}
47
48func (m *Module) Help() string {
49 return "USAGE: !tweet TEXT || !reply ID @HANDLE TEXT || !directmsg USER TEXT || !retweet ID || !favorite ID || !stat ID"
50}
51
52func (m *Module) Defaults() {
53 m.ReadOnly = false
54}
55
56func (m *Module) Load(client *irc.Client) error {
57 anaconda.SetConsumerKey(m.ConsumerKey)
58 anaconda.SetConsumerSecret(m.ConsumerSecret)
59
60 m.api = anaconda.NewTwitterApi(m.AccessToken, m.AccessTokenSecret)
61 client.CmdHook("privmsg", m.statCmd)
62
63 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 }
70
71 values := url.Values{}
72 values.Add("skip_status", "true")
73
74 user, err := m.api.GetSelf(values)
75 if err != nil {
76 return err
77 } else {
78 m.user = user
79 }
80
81 values = url.Values{}
82 values.Add("replies", "all")
83 values.Add("with", "user")
84
85 go func(c *irc.Client, v url.Values) {
86 for {
87 m.streamHandler(c, v)
88 }
89 }(client, values)
90
91 return nil
92}
93
94func (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 nil
103 }
104}
105
106func (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 nil
110 }
111
112 status := strings.Join(splited[1:], " ")
113 return m.tweet(status, url.Values{}, client, msg)
114}
115
116func (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 nil
120 }
121
122 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 }
127
128 values := url.Values{}
129 values.Add("in_reply_to_status_id", splited[1])
130
131 return m.tweet(status, values, client, msg)
132}
133
134func (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 nil
138 }
139
140 id, err := strconv.Atoi(splited[1])
141 if err != nil {
142 return err
143 }
144
145 if _, err := m.api.Retweet(int64(id), false); err != nil {
146 return client.Write("NOTICE %s :ERROR: %s",
147 msg.Receiver, err.Error())
148 }
149
150 return nil
151}
152
153func (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 nil
157 }
158
159 id, err := strconv.Atoi(splited[1])
160 if err != nil {
161 return err
162 }
163
164 if _, err := m.api.Favorite(int64(id)); err != nil {
165 return client.Write("NOTICE %s :ERROR: %s",
166 msg.Receiver, err.Error())
167 }
168
169 return nil
170}
171
172func (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 nil
176 }
177
178 scname := splited[1]
179 status := strings.Join(splited[2:], " ")
180
181 if _, err := m.api.PostDMToScreenName(status, scname); err != nil {
182 return client.Write("NOTICE %s :ERROR: %s",
183 msg.Receiver, err.Error())
184 }
185
186 return nil
187}
188
189func (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 nil
193 }
194
195 id, err := strconv.Atoi(splited[1])
196 if err != nil {
197 return err
198 }
199
200 tweet, err := m.api.GetTweet(int64(id), url.Values{})
201 if err != nil {
202 return err
203 }
204
205 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}
208
209func (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 break
216 }
217
218 if t := m.formatEvent(event); len(t) > 0 {
219 m.notify(client, t)
220 }
221 }
222 }
223
224 stream.Stop()
225}
226
227func (m *Module) formatEvent(event interface{}) string {
228 var msg string
229 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 break
240 }
241
242 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 break
247 }
248
249 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 }
253
254 return msg
255}
256
257func (m *Module) notify(client *irc.Client, text string) {
258 for _, ch := range client.Channels {
259 client.Write("NOTICE %s :%s", ch, text)
260 }
261}