Compare commits

...

10 Commits

5 changed files with 149 additions and 106 deletions

124
main.go
View File

@ -32,7 +32,10 @@ func main() {
var soundcloudClientId string var soundcloudClientId string
var soundcloudClientSecret string var soundcloudClientSecret string
var webEnableImages bool
var debug bool var debug bool
var noInvite bool
var useTLS bool var useTLS bool
var server string var server string
var password string var password string
@ -48,6 +51,7 @@ func main() {
kingpin.Flag("nick", "The nickname.").Short('n').StringVar(&nickname) kingpin.Flag("nick", "The nickname.").Short('n').StringVar(&nickname)
kingpin.Flag("ident", "The ident.").Short('i').StringVar(&ident) kingpin.Flag("ident", "The ident.").Short('i').StringVar(&ident)
kingpin.Flag("debug", "Enables debug mode.").Short('d').BoolVar(&debug) kingpin.Flag("debug", "Enables debug mode.").Short('d').BoolVar(&debug)
kingpin.Flag("no-invite", "Disables auto-join on invite.").BoolVar(&noInvite)
kingpin.Flag("tls", "Use TLS.").BoolVar(&useTLS) kingpin.Flag("tls", "Use TLS.").BoolVar(&useTLS)
kingpin.Flag("server", "The server to connect to.").Short('s').StringVar(&server) kingpin.Flag("server", "The server to connect to.").Short('s').StringVar(&server)
kingpin.Flag("password", "The password to use for logging into the IRC server.").Short('p').StringVar(&password) kingpin.Flag("password", "The password to use for logging into the IRC server.").Short('p').StringVar(&password)
@ -58,9 +62,14 @@ func main() {
// Youtube config // Youtube config
kingpin.Flag("youtube-key", "The API key to use to access the YouTube API.").StringVar(&youtubeApiKey) kingpin.Flag("youtube-key", "The API key to use to access the YouTube API.").StringVar(&youtubeApiKey)
// SoundCloud config
kingpin.Flag("soundcloud-id", "The SoundCloud ID.").StringVar(&soundcloudClientId) kingpin.Flag("soundcloud-id", "The SoundCloud ID.").StringVar(&soundcloudClientId)
kingpin.Flag("soundcloud-secret", "The SoundCloud secret.").StringVar(&soundcloudClientSecret) kingpin.Flag("soundcloud-secret", "The SoundCloud secret.").StringVar(&soundcloudClientSecret)
// Web parser config
kingpin.Flag("images", "Enables parsing links of images. Disabled by default for legal reasons.").BoolVar(&webEnableImages)
kingpin.Parse() kingpin.Parse()
if len(nickname) == 0 { if len(nickname) == 0 {
@ -74,25 +83,36 @@ func main() {
m := manager.NewManager() m := manager.NewManager()
// Load youtube parser // Load youtube parser
youtubeParser := &youtube.Parser{ if len(youtubeApiKey) > 0 {
Config: &youtube.Config{ApiKey: youtubeApiKey}, youtubeParser := &youtube.Parser{
Config: &youtube.Config{ApiKey: youtubeApiKey},
}
must(m.RegisterParser(youtubeParser))
} else {
log.Println("No YouTube API key provided, YouTube parsing via API is disabled.")
} }
must(m.RegisterParser(youtubeParser))
// Load soundcloud parser // Load soundcloud parser
soundcloudParser := &soundcloud.Parser{ if len(soundcloudClientId) > 0 && len(soundcloudClientSecret) > 0 {
Config: &soundcloud.Config{ soundcloudParser := &soundcloud.Parser{
ClientId: soundcloudClientId, Config: &soundcloud.Config{
ClientSecret: soundcloudClientSecret, ClientId: soundcloudClientId,
}, ClientSecret: soundcloudClientSecret,
},
}
must(m.RegisterParser(soundcloudParser))
} else {
log.Println("No SoundCloud client ID or secret provided, SoundCloud parsing via API is disabled.")
} }
must(m.RegisterParser(soundcloudParser))
// Load wikipedia parser // Load wikipedia parser
must(m.RegisterParser(new(wikipedia.Parser))) must(m.RegisterParser(new(wikipedia.Parser)))
// Load web parser // Load web parser
must(m.RegisterParser(new(web.Parser))) webParser := &web.Parser{
EnableImages: webEnableImages,
}
must(m.RegisterParser(webParser))
// IRC // IRC
conn := m.AntifloodIrcConn(irc.IRC(nickname, ident)) conn := m.AntifloodIrcConn(irc.IRC(nickname, ident))
@ -129,6 +149,8 @@ func main() {
conn.AddCallback("JOIN", func(e *irc.Event) { conn.AddCallback("JOIN", func(e *irc.Event) {
// Is this JOIN not about us? // Is this JOIN not about us?
if !strings.EqualFold(e.Nick, conn.GetNick()) { if !strings.EqualFold(e.Nick, conn.GetNick()) {
// Save this user's details for a temporary ignore
m.NotifyUserJoined(e.Arguments[0], e.Source)
return return
} }
@ -138,44 +160,46 @@ func main() {
default: default:
} }
}) })
conn.AddCallback("INVITE", func(e *irc.Event) { if !noInvite {
// Is this INVITE not for us? conn.AddCallback("INVITE", func(e *irc.Event) {
if !strings.EqualFold(e.Arguments[0], conn.GetNick()) { // Is this INVITE not for us?
return if !strings.EqualFold(e.Arguments[0], conn.GetNick()) {
} return
// Asynchronous notification
select {
case inviteChan <- e.Arguments[1]:
default:
}
// We have been invited, autojoin!
go func(sourceNick string, targetChannel string) {
joinWaitLoop:
for {
select {
case channel := <-joinChan:
if strings.EqualFold(channel, targetChannel) {
// TODO - Thanks message
time.Sleep(1 * time.Second)
conn.Privmsgf(targetChannel, "Thanks for inviting me, %s! I am %s, the friendly bot that shows information about links posted in this channel. I hope I can be of great help for everyone here in %s! :)", sourceNick, conn.GetNick(), targetChannel)
time.Sleep(2 * time.Second)
conn.Privmsg(targetChannel, "If you ever run into trouble with me (or find any bugs), please use the channel #MediaLink for contact on this IRC.")
break joinWaitLoop
}
case channel := <-inviteChan:
if strings.EqualFold(channel, targetChannel) {
break joinWaitLoop
}
case <-time.After(time.Minute):
log.Printf("WARNING: Timed out waiting for us to join %s as we got invited", targetChannel)
break joinWaitLoop
}
} }
}(e.Nick, e.Arguments[1])
conn.Join(e.Arguments[1]) // Asynchronous notification
}) select {
case inviteChan <- e.Arguments[1]:
default:
}
// We have been invited, autojoin!
go func(sourceNick string, targetChannel string) {
joinWaitLoop:
for {
select {
case channel := <-joinChan:
if strings.EqualFold(channel, targetChannel) {
// TODO - Thanks message
time.Sleep(1 * time.Second)
conn.Privmsgf(targetChannel, "Thanks for inviting me, %s! I am %s, the friendly bot that shows information about links posted in this channel. I hope I can be of great help for everyone here in %s! :)", sourceNick, conn.GetNick(), targetChannel)
time.Sleep(2 * time.Second)
conn.Privmsg(targetChannel, "If you ever run into trouble with me (or find any bugs), please use the channel #MediaLink for contact on this IRC.")
break joinWaitLoop
}
case channel := <-inviteChan:
if strings.EqualFold(channel, targetChannel) {
break joinWaitLoop
}
case <-time.After(time.Minute):
log.Printf("WARNING: Timed out waiting for us to join %s as we got invited", targetChannel)
break joinWaitLoop
}
}
}(e.Nick, e.Arguments[1])
conn.Join(e.Arguments[1])
})
}
conn.AddCallback("PRIVMSG", func(e *irc.Event) { conn.AddCallback("PRIVMSG", func(e *irc.Event) {
go func(event *irc.Event) { go func(event *irc.Event) {
//sender := event.Nick //sender := event.Nick
@ -196,6 +220,12 @@ func main() {
log.Printf("<%s @ %s> %s", event.Nick, target, msg) log.Printf("<%s @ %s> %s", event.Nick, target, msg)
// Ignore user if they just joined
if shouldIgnore := m.TrackUser(target, event.Source); shouldIgnore {
log.Print("This message will be ignored since the user just joined.")
return
}
urlStr := xurls.Relaxed.FindString(msg) urlStr := xurls.Relaxed.FindString(msg)
switch { switch {

View File

@ -67,19 +67,22 @@
({{ . }}) ({{ . }})
{{ end }} {{ end }}
{{ else }} {{ else }}
{{ if index . "Description" }} {{ with index . "Description" }}
{{ excerpt 384 (index . "Description") }} {{ excerpt 384 . }}
{{ else }} {{ end }}
{{ with index . "ImageType" }} {{ end }}
{{ . }} image,
{{ if index . "ImageType" }}
{{ if index . "Title" }}
·
{{ end }}
{{ .ImageType }} image,
{{ if (index . "ImageSize") (index . "Size") }}
{{ with index . "ImageSize" }}
{{ .X }}×{{ .Y }}
{{ end }} {{ end }}
{{ if (index . "ImageSize") (index . "Size") }} {{ with index . "Size" }}
{{ with index . "ImageSize" }} ({{ size . }})
{{ .X }}×{{ .Y }}
{{ end }}
{{ with index . "Size" }}
({{ size . }})
{{ end }}
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ end }} {{ end }}

View File

@ -17,6 +17,28 @@ func (m *Manager) initAntiflood() {
m.cache = cache.New(1*time.Minute, 5*time.Second) m.cache = cache.New(1*time.Minute, 5*time.Second)
} }
func (m *Manager) TrackUser(target string, source string) (shouldIgnore bool) {
key := normalizeUserAntiflood(target, source)
if _, ok := m.cache.Get(key); ok {
// User just joined here recently, ignore them
shouldIgnore = true
}
return
}
func (m *Manager) NotifyUserJoined(target string, source string) {
key := normalizeUserAntiflood(target, source)
// When a user joins, he will be ignored for the first 30 seconds,
// enough to prevent parsing links from people who only join to spam their
// links immediately
if _, exists := m.cache.Get(key); !exists {
m.cache.Add(key, nil, 30*time.Second)
}
}
func (m *Manager) TrackUrl(target string, u *url.URL) (shouldIgnore bool) { func (m *Manager) TrackUrl(target string, u *url.URL) (shouldIgnore bool) {
key := normalizeUrlAntiflood(target, u) key := normalizeUrlAntiflood(target, u)
@ -70,6 +92,17 @@ func normalizeTextAntiflood(target, text string) string {
return fmt.Sprintf("TEXT/%s/%X", strings.ToUpper(target), s.Sum([]byte{})) return fmt.Sprintf("TEXT/%s/%X", strings.ToUpper(target), s.Sum([]byte{}))
} }
func normalizeUserAntiflood(target, source string) string {
sourceSplitHost := strings.SplitN(source, "@", 2)
sourceSplitHostname := strings.Split(sourceSplitHost[1], ".")
if len(sourceSplitHostname) > 1 &&
strings.EqualFold(sourceSplitHostname[len(sourceSplitHostname)-1], "IP") {
sourceSplitHostname[0] = "*"
}
source = fmt.Sprintf("%s!%s@%s", "*", "*", strings.Join(sourceSplitHostname, "."))
return fmt.Sprintf("USER/%s/%s", strings.ToUpper(target), source)
}
// Proxies several methods of the IRC connection in order to drop repeated messages // Proxies several methods of the IRC connection in order to drop repeated messages
type ircConnectionProxy struct { type ircConnectionProxy struct {
*irc.Connection *irc.Connection

View File

@ -2,7 +2,6 @@ package web
import ( import (
"errors" "errors"
"log"
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
@ -33,7 +32,9 @@ const (
maxHtmlSize = 8 * 1024 maxHtmlSize = 8 * 1024
) )
type Parser struct{} type Parser struct {
EnableImages bool
}
func (p *Parser) Init() error { func (p *Parser) Init() error {
return nil return nil
@ -65,6 +66,7 @@ func (p *Parser) Parse(u *url.URL, referer *url.URL) (result parsers.ParseResult
if referer != nil { if referer != nil {
req.Header.Set("Referer", referer.String()) req.Header.Set("Referer", referer.String())
} }
req.Header.Set("User-Agent", "MediaLink IRC Bot")
if resp, err := http.DefaultTransport.RoundTrip(req); err != nil { if resp, err := http.DefaultTransport.RoundTrip(req); err != nil {
result.Error = err result.Error = err
return return
@ -119,23 +121,28 @@ func (p *Parser) Parse(u *url.URL, referer *url.URL) (result parsers.ParseResult
result.Information[0]["Title"] = noTitleStr result.Information[0]["Title"] = noTitleStr
} }
case "image/png", "image/jpeg", "image/gif": case "image/png", "image/jpeg", "image/gif":
if p.EnableImages {
// No need to limit the reader to a specific size here as // No need to limit the reader to a specific size here as
// image.DecodeConfig only reads as much as needed anyways. // image.DecodeConfig only reads as much as needed anyways.
if m, imgType, err := image.DecodeConfig(resp.Body); err != nil { if m, imgType, err := image.DecodeConfig(resp.Body); err != nil {
result.UserError = ErrCorruptedImage result.UserError = ErrCorruptedImage
} else { } else {
info := map[string]interface{}{ info := map[string]interface{}{
"IsUpload": true, "IsUpload": true,
"ImageSize": image.Point{X: m.Width, Y: m.Height}, "ImageSize": image.Point{X: m.Width, Y: m.Height},
"ImageType": strings.ToUpper(imgType), "ImageType": strings.ToUpper(imgType),
"Title": u.Path[strings.LastIndex(u.Path, "/")+1:],
}
if resp.ContentLength > 0 {
info["Size"] = uint64(resp.ContentLength)
}
result.Information = []map[string]interface{}{info}
} }
if resp.ContentLength > 0 { break
info["Size"] = uint64(resp.ContentLength)
}
result.Information = []map[string]interface{}{info}
log.Printf("Got through: %+v!", info)
} }
fallthrough
default: default:
// TODO - Implement generic head info? // TODO - Implement generic head info?
result.Ignored = true result.Ignored = true

View File

@ -1,33 +1,3 @@
package main package main
import ( // TODO - unit test stripIrcFormatting
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func mustParseUrl(u string) *url.URL {
if uri, err := url.Parse(u); err == nil {
return uri
} else {
panic(err)
}
}
func Test_GetYouTubeId(t *testing.T) {
assert.Equal(t, "aYz-9jUlav-", getYouTubeId(mustParseUrl("http://youtube.com/watch?v=aYz-9jUlav-")))
assert.Equal(t, "aYz-9jUlav-", getYouTubeId(mustParseUrl("https://youtube.com/watch?v=aYz-9jUlav-")))
assert.Equal(t, "aYz-9jUlav-", getYouTubeId(mustParseUrl("http://www.youtube.com/watch?v=aYz-9jUlav-")))
assert.Equal(t, "aYz-9jUlav-", getYouTubeId(mustParseUrl("https://www.youtube.com/watch?v=aYz-9jUlav-")))
assert.Equal(t, "aYz-9jUlav-", getYouTubeId(mustParseUrl("http://youtu.be/aYz-9jUlav-")))
assert.Equal(t, "aYz-9jUlav-", getYouTubeId(mustParseUrl("https://youtu.be/aYz-9jUlav-")))
assert.Equal(t, "aYz-9jUlav-", getYouTubeId(mustParseUrl("http://www.youtu.be/aYz-9jUlav-")))
assert.Equal(t, "aYz-9jUlav-", getYouTubeId(mustParseUrl("https://www.youtu.be/aYz-9jUlav-")))
}
func Benchmark_GetYouTubeId(b *testing.B) {
for n := 0; n < b.N; n++ {
getYouTubeId(mustParseUrl("http://youtube.com/watch?v=aYz-9jUlav-"))
}
}