Compare commits
10 Commits
Author | SHA1 | Date |
---|---|---|
|
c68a24193c | |
|
a111a35e66 | |
|
518dbeaf22 | |
|
bd63c975b8 | |
|
3faff8264f | |
|
1e3af32706 | |
|
b37a15ac1f | |
|
6d70e02641 | |
|
19708251b9 | |
|
fb85ad8554 |
124
main.go
124
main.go
|
@ -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 {
|
||||||
|
|
27
main.tpl
27
main.tpl
|
@ -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 }}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
32
util_test.go
32
util_test.go
|
@ -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-"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue