Skip to content
Extraits de code Groupes Projets
Valider 781a9377 rédigé par Alexandre Morignot's avatar Alexandre Morignot
Parcourir les fichiers

Basic working bot

parent
Branches
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
package main
import (
"flag"
"io/ioutil"
"log"
"reflect"
"strings"
"gopkg.in/yaml.v2"
"git.iiens.net/morignot2011/jc"
"git.iiens.net/morignot2011/jc/irc"
"git.iiens.net/morignot2011/jc/slack"
)
type Cfg struct {
Transports map[string]map[string]interface{}
Links []map[string]interface{}
}
type Links map[string]Link
type Link map[string]LinkDest
type LinkDest struct {
transport string
channel string
filter []string
}
var transports = make(map[string]jc.Transport)
var links = make(Links)
func main() {
var cfgFile = flag.String("config", "jc.conf", "path to configuration file")
flag.Parse()
file, err := ioutil.ReadFile(*cfgFile)
if err != nil {
log.Fatal(err)
}
var cfg Cfg
err = yaml.Unmarshal(file, &cfg)
if err != nil {
log.Fatal(err)
}
for name, transport := range cfg.Transports {
type_, ok := transport["type"]
if !ok {
log.Fatalf("Missing 'type' for transport %s", name)
}
delete(transport, "type")
var t jc.Transport
var err error
switch type_.(string) {
case "irc":
t, err = irc.New(transport)
case "slack":
t, err = slack.New(transport)
}
if err != nil {
log.Fatalf("Error when creating transport %s: %s", name, err)
}
transports[name] = t
err = t.Run()
if err != nil {
log.Fatalf("Error when connecting transport %s: %s", name, err)
}
}
for _, link := range cfg.Links {
filter, ok := link["filter"]
if !ok {
filter = []string{}
}
delete(link, "filter")
for a, b := range link {
cfgA := strings.Split(a, "@")
cfgB := strings.Split(b.(string), "@")
if len(cfgA) != 2 || len(cfgB) != 2 {
log.Fatalf("Invalid link '%s => %s', it must be at the format 'channelA@transportA => channelB@transportB'", a, b.(string))
}
if _, ok := transports[cfgA[1]]; !ok {
log.Fatalf("Unknown transport %s in link '%s => %s'", cfgA[1], a, b.(string))
}
if _, ok := transports[cfgB[1]]; !ok {
log.Fatalf("Unknown transport %s in link '%s => %s'", cfgB[1], a, b.(string))
}
var filters []string
for _, f := range filter.([]interface{}) {
filters = append(filters, f.(string))
}
if links[cfgA[1]] == nil {
links[cfgA[1]] = make(Link)
}
links[cfgA[1]][cfgA[0]] = LinkDest{
transport: cfgB[1],
channel: cfgB[0],
filter: filters,
}
if links[cfgB[1]] == nil {
links[cfgB[1]] = make(Link)
}
links[cfgB[1]][cfgB[0]] = LinkDest{
transport: cfgA[1],
channel: cfgA[0],
filter: filters,
}
}
}
// remove sensitive data
cfg = Cfg{}
cases := make([]reflect.SelectCase, len(transports)*2)
i := 0
for _, t := range transports {
cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(t.GetEvents())}
i++
cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(t.GetEnd())}
i++
}
for {
idx, value, ok := reflect.Select(cases)
i := 0
var name string
for name, _ = range transports {
if i == idx || i+1 == idx {
break
}
i += 2
}
if !ok {
log.Fatalf("Transport %s close one of its channel!", name)
}
switch ev := value.Interface().(type) {
case *jc.JoinEvent:
dest, ok := links[name][ev.Channel]
if !ok {
continue
}
if isFiltered(dest.filter, ev.Nick) {
continue
}
t := transports[dest.transport]
go t.Join(&jc.JoinEvent{
Nick: ev.Nick + "_jc",
Channel: dest.channel,
})
case *jc.MessageEvent:
dest, ok := links[name][ev.Channel]
if !ok {
continue
}
if isFiltered(dest.filter, ev.Nick) {
continue
}
t := transports[dest.transport]
go t.Message(&jc.MessageEvent{
Nick: ev.Nick + "_jc",
Channel: dest.channel,
Text: ev.Text,
})
case bool:
log.Fatalf("Disconnected from %s", name)
return
}
}
}
func isFiltered(filter []string, nick string) bool {
for _, v := range filter {
if v == nick {
return true
}
}
return false
}
package jc
import "fmt"
type ConfigError struct {
Field string
}
func (e ConfigError) Error() string {
return fmt.Sprintf("Missing field '%s' in configuration", e.Field)
}
type ConnectionError struct {
Err error
}
func (e ConnectionError) Error() string {
return fmt.Sprintf("Connection error: %s", e.Err)
}
package jc
type JoinEvent struct {
Nick string
Channel string
}
type MessageEvent struct {
Nick string
Channel string
Text string
}
type NickEvent struct {
OldNick string
NewNick string
}
type PrivMessageEvent struct {
Nick string
Channel string
Text string
}
type PartEvent struct {
Nick string
Channel string
}
type QuitEvent struct {
Nick string
}
package irc
import (
"log"
"git.iiens.net/morignot2011/jc"
)
func (t *Transport) Join(ev *jc.JoinEvent) {
if i := FindChannel(t.channels, ev.Channel); i == -1 {
// cannot join a channel not configured
return
}
client, ok := t.userClients[ev.Nick]
if !ok {
ircCfg, err := t.newIrcConfig(ev.Nick, t.cfg)
if err != nil {
log.Print(err)
return
}
client = t.getIrcClient(ircCfg)
t.userClients[ev.Nick] = client
t.userChannels[ev.Nick] = []string{ev.Channel}
if err := client.Connect(); err != nil {
log.Print(err)
return
}
} else {
// add channel to the user list
if i := FindChannel(t.userChannels[ev.Nick], ev.Channel); i == -1 {
t.userChannels[ev.Nick] = append(t.userChannels[ev.Nick], ev.Channel)
client.Join(ev.Channel)
}
}
}
func (t *Transport) Message(ev *jc.MessageEvent) {
client, ok := t.userClients[ev.Nick]
if !ok {
// unknown client
return
}
if i := FindChannel(t.userChannels[ev.Nick], ev.Channel); i == -1 {
// this user is not on this channel
return
}
client.Privmsg(ev.Channel, ev.Text)
}
func (t *Transport) Nick(ev *jc.NickEvent) {
client, ok := t.userClients[ev.OldNick]
if !ok {
// unknown client
return
}
t.realNicks[client.Me().Nick] = ev.NewNick
}
func (t *Transport) PrivMessage(ev *jc.PrivMessageEvent) {
client, ok := t.userClients[ev.Nick]
if !ok {
// unknown client
return
}
client.Privmsg(ev.Channel, ev.Text)
}
func (t *Transport) Part(ev *jc.PartEvent) {
client, ok := t.userClients[ev.Nick]
if !ok {
// unknown client
return
}
i := FindChannel(t.userChannels[ev.Nick], ev.Channel)
if i := FindChannel(t.userChannels[ev.Nick], ev.Channel); i == -1 {
// this user is not on this channel
return
}
client.Part(ev.Channel)
// remove chan from user's
t.userChannels[ev.Nick] = append(t.userChannels[ev.Nick][:i], t.userChannels[ev.Nick][i+1:]...)
// make the user quit if is not in anymore channel
if len(t.userChannels) == 0 {
t.Quit(&jc.QuitEvent{ev.Nick})
}
}
func (t *Transport) Quit(ev *jc.QuitEvent) {
client, ok := t.userClients[ev.Nick]
if !ok {
// unknown client
return
}
client.Quit()
delete(t.userClients, ev.Nick)
delete(t.userChannels, ev.Nick)
delete(t.realNicks, client.Me().Nick)
}
package irc
import (
//"fmt"
"log"
"strings"
//"time"
irc "github.com/fluffle/goirc/client"
"git.iiens.net/morignot2011/jc"
)
func (t *Transport) connected(client *irc.Conn, line *irc.Line) {
if t.client != client {
// user's client
for _, channel := range t.channels {
log.Printf("join %s", channel)
client.Join(channel)
}
} else {
// bot's client
//st := t.client.StateTracker()
//me := st.Me()
for _, channel := range t.channels {
t.client.Join(channel)
/*
time.Sleep(time.Second * 2)
if _, ok := me.Channels[channel]; !ok {
t.connectionError <- fmt.Errorf("Cannot join channel %s", channel)
break
}
*/
}
close(t.connectionError)
}
}
func (t *Transport) disconnected(client *irc.Conn, line *irc.Line) {
if t.client != client {
// all should already have been cleaned
log.Print("%s got disconnected", client.Me())
return
}
// bot's client
t.End <- true
}
func (t *Transport) join(client *irc.Conn, line *irc.Line) {
if t.client != client || t.isUserDistant(line.Nick) {
return
}
me := t.client.StateTracker().Me()
if line.Nick == me.Nick {
return
}
// bot's client
t.Events <- &jc.JoinEvent{
Nick: line.Nick,
Channel: line.Args[0],
}
}
func (t *Transport) nick(client *irc.Conn, line *irc.Line) {
if t.client != client || t.isUserDistant(line.Nick) {
return
}
// bot's client
t.Events <- &jc.NickEvent{
OldNick: line.Nick,
NewNick: line.Args[0],
}
}
func (t *Transport) part(client *irc.Conn, line *irc.Line) {
if t.client != client || t.isUserDistant(line.Nick) {
return
}
// bot's client
for _, channel := range line.Args {
t.Events <- &jc.PartEvent{
Nick: line.Nick,
Channel: channel,
}
}
}
func (t *Transport) privmsg(client *irc.Conn, line *irc.Line) {
text := line.Args[len(line.Args)-1]
if t.client != client {
for _, target := range line.Args[:len(line.Args)-1] {
if target[0] == '#' || target[0] == '$' {
continue
}
// this is a query
t.Events <- &jc.PrivMessageEvent{
Nick: line.Nick,
Channel: target,
Text: text,
}
}
} else if !t.isUserDistant(line.Nick) {
// bot's client
for _, target := range line.Args[:len(line.Args)-1] {
if target[0] != '#' || strings.Contains(target, ".") {
// Something else than # is either a server mask or a query
// # with a . in the target is a host mask
continue
}
t.Events <- &jc.MessageEvent{
Nick: line.Nick,
Channel: target,
Text: text,
}
}
}
}
func (t *Transport) quit(client *irc.Conn, line *irc.Line) {
if t.client != client || t.isUserDistant(line.Nick) {
return
}
// bot's client
t.Events <- &jc.QuitEvent{
Nick: line.Nick,
}
}
package irc
import (
"crypto/tls"
"fmt"
"log"
irc "github.com/fluffle/goirc/client"
"git.iiens.net/morignot2011/jc"
)
const (
DEFAULT_NICK = "jc"
DEFAULT_SSL = false
)
type Transport struct {
jc.BaseTransport
cfg map[string]interface{}
channels []string
client *irc.Conn
realNicks map[string]string
userClients map[string]*irc.Conn
userChannels map[string][]string
connectionError chan error
}
func New(cfg map[string]interface{}) (jc.Transport, error) {
t := &Transport{
cfg: cfg,
connectionError: make(chan error),
userClients: make(map[string]*irc.Conn),
userChannels: make(map[string][]string),
BaseTransport: jc.BaseTransport{
Events: make(chan interface{}),
End: make(chan bool),
},
}
ircCfg, err := t.newIrcConfig(DEFAULT_NICK, cfg)
if err != nil {
return nil, err
}
t.client = t.getIrcClient(ircCfg)
channels, ok := cfg["channels"]
if !ok {
return nil, jc.ConfigError{"channels"}
}
for _, channel := range channels.([]interface{}) {
t.channels = append(t.channels, channel.(string))
}
return t, nil
}
func (t *Transport) Run() error {
if err := t.client.Connect(); err != nil {
return jc.ConnectionError{err}
}
if err, ok := <-t.connectionError; ok {
return jc.ConnectionError{err}
}
return nil
}
func (t *Transport) newIrcConfig(nick string, cfg map[string]interface{}) (*irc.Config, error) {
server, ok := cfg["server"]
if !ok {
return nil, jc.ConfigError{"server"}
}
port, ok := cfg["port"]
if !ok {
return nil, jc.ConfigError{"port"}
}
ssl, ok := cfg["ssl"]
if !ok {
ssl = DEFAULT_SSL
}
insecure, ok := cfg["ssl_insecure"]
if !ok {
insecure = false
}
ircCfg := irc.NewConfig(nick)
ircCfg.SSL = ssl.(bool)
if ircCfg.SSL {
ircCfg.SSLConfig = &tls.Config{
ServerName: server.(string),
InsecureSkipVerify: insecure.(bool),
}
}
ircCfg.Server = fmt.Sprintf("%s:%d", server, port)
ircCfg.NewNick = t.newNick
return ircCfg, nil
}
func (t *Transport) newNick(nick string) string {
log.Printf("new nick %s", nick)
realNick, ok := t.realNicks[nick]
if !ok {
realNick = nick
} else {
delete(t.realNicks, nick)
}
newNick := nick + "_"
t.realNicks[newNick] = realNick
return newNick
}
func (t *Transport) getIrcClient(cfg *irc.Config) *irc.Conn {
client := irc.Client(cfg)
client.EnableStateTracking()
client.HandleFunc(irc.CONNECTED, t.connected)
client.HandleFunc(irc.DISCONNECTED, t.disconnected)
client.HandleFunc(irc.JOIN, t.join)
client.HandleFunc(irc.NICK, t.nick)
client.HandleFunc(irc.PART, t.part)
client.HandleFunc(irc.PRIVMSG, t.privmsg)
client.HandleFunc(irc.QUIT, t.quit)
return client
}
func (t *Transport) isUserDistant(user string) bool {
for k, _ := range t.userClients {
if k == user {
return true
}
}
return false
}
package irc
func FindChannel(channels []string, needle string) int {
for i, channel := range channels {
if channel == needle {
return i
}
}
return -1
}
package slack
import (
"fmt"
"git.iiens.net/morignot2011/jc"
)
func (t *Transport) Join(ev *jc.JoinEvent) {
text := fmt.Sprintf("%s has joined the channel", ev.Nick)
channel := t.channelIDs[ev.Channel]
t.rtm.SendMessage(t.rtm.NewOutgoingMessage(text, channel))
}
func (t *Transport) Message(ev *jc.MessageEvent) {
text := fmt.Sprintf("<%s> %s", ev.Nick, ev.Text)
channel := t.channelIDs[ev.Channel]
t.rtm.SendMessage(t.rtm.NewOutgoingMessage(text, channel))
}
func (t *Transport) Nick(ev *jc.NickEvent) {
text := fmt.Sprintf("%s is now known as%s", ev.OldNick, ev.NewNick)
for _, channel := range t.channelIDs {
t.rtm.SendMessage(t.rtm.NewOutgoingMessage(text, channel))
}
}
func (t *Transport) PrivMessage(ev *jc.PrivMessageEvent) {
// TODO
}
func (t *Transport) Part(ev *jc.PartEvent) {
text := fmt.Sprintf("%s has left", ev.Nick)
channel := t.channelIDs[ev.Channel]
t.rtm.SendMessage(t.rtm.NewOutgoingMessage(text, channel))
}
func (t *Transport) Quit(ev *jc.QuitEvent) {
text := fmt.Sprintf("%s has quit", ev.Nick)
for _, channel := range t.channelIDs {
t.rtm.SendMessage(t.rtm.NewOutgoingMessage(text, channel))
}
}
package slack
import (
"fmt"
"github.com/nlopes/slack"
"git.iiens.net/morignot2011/jc"
)
func (t *Transport) connected(ev *slack.ConnectedEvent) {
events := []*jc.JoinEvent{}
for name, id := range t.channelIDs {
channel, err := t.api.GetChannelInfo(id)
if err != nil {
t.connectionError <- err
return
}
fmt.Printf("%s: %d\n", name, len(channel.Members))
for _, id := range channel.Members {
user, err := t.api.GetUserInfo(id)
if err != nil {
t.connectionError <- err
return
}
t.userNames[id] = user.Name
events = append(events, &jc.JoinEvent{
Nick: user.Name,
Channel: name,
})
}
}
close(t.connectionError)
fmt.Printf("joins: %d\n", len(events))
for _, event := range events {
t.Events <- event
}
}
func (t *Transport) invalidAuth(ev *slack.InvalidAuthEvent) {
t.connectionError <- fmt.Errorf("Invalid auth")
}
func (t *Transport) message(ev *slack.MessageEvent) {
channel, ok := t.channelNames[ev.Channel]
if !ok {
// probably we got invited on a channel after starting the bot
return
}
nick, ok := t.userNames[ev.User]
if !ok {
// probably a user which have join after connection
return
}
t.Events <- &jc.MessageEvent{
Nick: nick,
Channel: channel,
Text: ev.Text,
}
}
package slack
import (
"github.com/nlopes/slack"
"git.iiens.net/morignot2011/jc"
)
type Transport struct {
jc.BaseTransport
api *slack.Client
rtm *slack.RTM
channelIDs map[string]string
channelNames map[string]string
connectionError chan error
userNames map[string]string
}
func New(cfg map[string]interface{}) (jc.Transport, error) {
token, ok := cfg["token"]
if !ok {
return nil, jc.ConfigError{"token"}
}
api := slack.New(token.(string))
rtm := api.NewRTM()
t := &Transport{
api: api,
rtm: rtm,
channelIDs: make(map[string]string),
channelNames: make(map[string]string),
connectionError: make(chan error),
userNames: make(map[string]string),
BaseTransport: jc.BaseTransport{
Events: make(chan interface{}),
End: make(chan bool),
},
}
channels, err := t.api.GetChannels(true)
if err != nil {
return nil, err
}
for _, channel := range channels {
if !channel.IsMember {
continue
}
t.channelIDs[channel.Name] = channel.ID
t.channelNames[channel.ID] = channel.Name
}
return t, nil
}
func (t *Transport) Run() error {
go t.rtm.ManageConnection()
go t.dispatchEvents()
if err, ok := <-t.connectionError; ok {
return jc.ConnectionError{err}
}
return nil
}
func (t *Transport) dispatchEvents() {
for {
select {
case msg := <-t.rtm.IncomingEvents:
switch ev := msg.Data.(type) {
case *slack.ChannelJoinedEvent:
// TODO
case *slack.ChannelRenameEvent:
// TODO
case *slack.ConnectedEvent:
t.connected(ev)
case *slack.MessageEvent:
t.message(ev)
case *slack.InvalidAuthEvent:
t.invalidAuth(ev)
case *slack.RTMError:
// Log
}
}
}
}
package jc
type Transport interface {
Run() (err error)
Join(*JoinEvent)
Message(*MessageEvent)
Nick(*NickEvent)
PrivMessage(*PrivMessageEvent)
Part(*PartEvent)
Quit(*QuitEvent)
GetEvents() chan interface{}
GetEnd() chan bool
}
type BaseTransport struct {
Events chan interface{}
End chan bool
}
func (t *BaseTransport) GetEvents() chan interface{} {
return t.Events
}
func (t *BaseTransport) GetEnd() chan bool {
return t.End
}
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Veuillez vous inscrire ou vous pour commenter