marvin

A simple and modular IRC bot

git clone https://git.8pit.net/marvin.git

  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}