commit ec67a610e3062d3e60891332f33ace4ad5a448bd Author: Alex Auvolat Date: Sun Feb 16 16:26:55 2020 +0100 First commit with working stub of IRC bridge diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90ed92e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +easybridge diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5f03483 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +all: + go build diff --git a/connector/config.go b/connector/config.go new file mode 100644 index 0000000..d719b49 --- /dev/null +++ b/connector/config.go @@ -0,0 +1,21 @@ +package connector + +import ( + "strconv" +) + +type Configuration map[string]string + +func (c Configuration) GetString(k string) string { + if ss, ok := c[k]; ok { + return ss + } + return "" +} + +func (c Configuration) GetInt(k string) (int, error) { + if ss, ok := c[k]; ok { + return strconv.Atoi(ss) + } + return 0, nil +} diff --git a/connector/connector.go b/connector/connector.go new file mode 100644 index 0000000..2116bb5 --- /dev/null +++ b/connector/connector.go @@ -0,0 +1,134 @@ +package connector + +/* + A generic connector framework for instant messaging protocols. + + Model: + + - A connector represents a connection to an outgoing service (IRC, XMPP, etc) + It satisfies a generic interface representing the actions that can be called + (send messages, join room, etc) + + - A handler represents a consumer of events happening on a connection + It satisfies a generic interface representing the events that can happend + (message received, rooms autojoined, etc) + + - A connector implements a given protocol that has an identifier + Each protocol identifier determines a namespace for user identifiers + and room identifiers which are globally unique for all connections using + this protocol. + For instance, a user can have two IRC conections to different servers. + Internally used user names and room identifiers must contain + the server name to be able to differentiate. +*/ + +type UserID string +type RoomID string + +type Connector interface { + // Set the handler that will receive events happening on this connection + SetHandler(handler Handler) + + // Configure (or reconfigure) the connector and attempt to connect + Configure(conf Configuration) error + + // Get the identifier of the protocol that is implemented by this connector + Protocol() string + + // Get the user id of the connected user + User() UserID + + // Set user information (nickname, picture, etc) + SetUserInfo(info *UserInfo) error + + // Set room information (name, description, picture, etc) + SetRoomInfo(roomId RoomID, info *RoomInfo) error + + // Try to join a channel + // If no error happens, it must fire a Handler.Joined event + Join(roomId RoomID) error + + // Leave a channel + Leave(roomId RoomID) + + // Send an event + Send(event *Event) error + + // Close the connection + Close() +} + +type Handler interface { + // Called when a room was joined (automatically or by call to Connector.Join) + Joined(roomId RoomID) + + // Called when the user left a room + Left(roomId RoomID) + + // Called when a user's info is updated (changed their nickname, status, etc) + // Can also be called with our own user ID when first loaded our user info + UserInfoUpdated(user UserID, info *UserInfo) + + // Called when a room's info was updated, + // or the first tome a room's info is retreived + RoomInfoUpdated(roomId RoomID, info *RoomInfo) + + // Called when an event occurs in a room + // This must not be called for events authored by the user of the connection + Event(event *Event) +} + +type EventType int +const ( + EVENT_JOIN EventType = iota + EVENT_LEAVE + EVENT_MESSAGE + EVENT_ACTION +) + +type Event struct { + Type EventType + + // UserID of the user that sent the event + // If this is a direct message event, this event can only have been authored + // by the user we are talking to (and not by ourself) + Author UserID + + // UserID of the targetted user in the case of a direct message, + // empty if targetting a room + Recipient UserID + + // RoomID of the room where the event happenned or of the targetted room, + // or empty string if it happenned by direct message + Room RoomID + + // Message text or action text + Message string + + // Attached files such as images + Attachements map[string]MediaObject +} + +type UserInfo struct { + Nickname string + Status string + Avatar MediaObject +} + +type RoomInfo struct { + Name string + Description string + Picture MediaObject +} + +type MediaObject interface { + Size() int + MimeType() string + + // AsBytes: must always be implemented + AsBytes() ([]byte, error) + + // AsString: not mandatory, may return an empty string + // If so, AsBytes() is the only way to retrieve the object + AsURL() string +} diff --git a/connector/irc/irc.go b/connector/irc/irc.go new file mode 100644 index 0000000..02f82eb --- /dev/null +++ b/connector/irc/irc.go @@ -0,0 +1,255 @@ +package irc + +import ( + "time" + "os" + "strings" + "fmt" + + . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" + + "github.com/lrstanley/girc" +) + +// User id format: nickname@server +// Room id format: #room_name@server + +type IRC struct { + handler Handler + config Configuration + + connected bool + timeout int + + nick string + name string + server string + conn *girc.Client +} + +func (irc *IRC) SetHandler(h Handler) { + irc.handler = h +} + +func(irc *IRC) Protocol() string { + return "irc" +} + +func (irc *IRC) Configure(c Configuration) error { + irc.config = c + + irc.nick = c.GetString("nick") + irc.server = c.GetString("server") + + port, err := c.GetInt("port") + if err != nil { + return err + } + if port == 0 { + port = 6667 + } + + client := girc.New(girc.Config{ + Server: irc.server, + Port: port, + Nick: irc.nick, + User: irc.nick, + Debug: os.Stderr, + SSL: true, + }) + + client.Handlers.Add(girc.CONNECTED, irc.ircConnected) + //client.Handlers.Add(girc.DISCONNECTED, irc.ircDisconnected) + //client.Handlers.Add(girc.NICK, irc.ircNick) + client.Handlers.Add(girc.PRIVMSG, irc.ircPrivmsg) + client.Handlers.Add(girc.JOIN, irc.ircJoin) + client.Handlers.Add(girc.PART, irc.ircPart) + client.Handlers.Add(girc.RPL_NAMREPLY, irc.ircNamreply) + client.Handlers.Add(girc.RPL_TOPIC, irc.ircTopic) + + irc.conn = client + go irc.connectLoop(client) + + for i := 0; i < 42; i++ { + time.Sleep(time.Duration(1)*time.Second) + if irc.conn != client { + break + } + if irc.connected { + return nil + } + } + return fmt.Errorf("Failed to conncect after 42s attempting") +} + +func (irc *IRC) User() UserID { + return UserID(irc.nick + "@" + irc.server) +} + +func (irc *IRC) checkRoomId(id RoomID) (string, error) { + x := strings.Split(string(id), "@") + if len(x) != 2 || x[1] != irc.server || x[0][0] != '#' { + return "", fmt.Errorf("Invalid room ID: %s", id) + } + return x[0], nil +} + +func (irc *IRC) checkUserId(id UserID) (string, error) { + x := strings.Split(string(id), "@") + if len(x) != 2 || x[1] != irc.server || x[0][0] == '#' { + return "", fmt.Errorf("Invalid user ID: %s", id) + } + return x[0], nil +} + +func (irc *IRC) SetUserInfo(info *UserInfo) error { + return fmt.Errorf("Not implemented") +} + +func (irc *IRC) SetRoomInfo(roomId RoomID, info *RoomInfo) error { + ch, err := irc.checkRoomId(roomId) + if err != nil { + return err + } + + if info.Name != "" && info.Name != ch { + return fmt.Errorf("May not change IRC room name to other than %s", ch) + } + if info.Picture != nil { + return fmt.Errorf("Room picture not supported on IRC") + } + irc.conn.Cmd.Topic(ch, info.Description) + return nil +} + +func (irc *IRC) Join(roomId RoomID) error { + ch, err := irc.checkRoomId(roomId) + if err != nil { + return err + } + + irc.conn.Cmd.Join(ch) + return nil +} + +func (irc *IRC) Leave(roomId RoomID) { + ch, err := irc.checkRoomId(roomId) + if err != nil { + return + } + + irc.conn.Cmd.Part(ch) +} + +func (irc *IRC) Send(event *Event) error { + dest := "" + if event.Room != "" { + ch, err := irc.checkRoomId(event.Room) + if err != nil { + return err + } + dest = ch + } else if event.Recipient != "" { + ui, err := irc.checkUserId(event.Recipient) + if err != nil { + return err + } + dest = ui + } else { + return fmt.Errorf("Invalid target") + } + + if event.Attachements != nil && len(event.Attachements) > 0 { + // TODO find a way to send them using some hosing of some kind + return fmt.Errorf("Attachements not supported on IRC") + } + + if event.Type == EVENT_MESSAGE { + irc.conn.Cmd.Message(dest, event.Message) + } else if event.Type == EVENT_ACTION { + irc.conn.Cmd.Action(dest, event.Message) + } else { + return fmt.Errorf("Invalid event type") + } + return nil +} + +func (irc *IRC) Close() { + irc.conn.Close() + irc.conn = nil +} + +func (irc *IRC) connectLoop(c *girc.Client) { + irc.timeout = 10 + for { + if irc.conn != c { + return + } + if err := c.Connect(); err != nil { + irc.connected = false + fmt.Printf("IRC failed to connect / disconnected: %s", err) + fmt.Printf("Retrying in %ds", irc.timeout) + time.Sleep(time.Duration(irc.timeout) * time.Second) + irc.timeout *= 2 + } else { + return + } + } +} + +func (irc *IRC) ircConnected(c *girc.Client, e girc.Event) { + fmt.Printf("ircConnected ^^^^\n") + irc.timeout = 10 + irc.connected = true +} + +func (irc *IRC) ircPrivmsg(c *girc.Client, e girc.Event) { + ev := &Event{ + Type: EVENT_MESSAGE, + Author: UserID(e.Source.Name + "@" + irc.server), + Message: e.Last(), + } + if e.IsFromChannel() { + ev.Room = RoomID(e.Params[0] + "@" + irc.server) + } + if e.IsAction() { + ev.Type = EVENT_ACTION + } + irc.handler.Event(ev) +} + +func (irc *IRC) ircJoin(c *girc.Client, e girc.Event) { + room := RoomID(e.Params[0] + "@" + irc.server) + if e.Source.Name == irc.nick { + irc.handler.Joined(room) + } else { + ev := &Event{ + Type: EVENT_JOIN, + Author: UserID(e.Source.Name + "@" + irc.server), + Room: room, + } + irc.handler.Event(ev) + } +} + +func (irc *IRC) ircPart(c *girc.Client, e girc.Event) { + room := RoomID(e.Params[0] + "@" + irc.server) + if e.Source.Name == irc.nick { + irc.handler.Left(room) + } else { + ev := &Event{ + Type: EVENT_LEAVE, + Author: UserID(e.Source.Name + "@" + irc.server), + Room: room, + } + irc.handler.Event(ev) + } +} + +func (irc *IRC) ircNamreply(c *girc.Client, e girc.Event) { + fmt.Printf("TODO namreply params: %#v", e.Params) +} + +func (irc *IRC) ircTopic(c *girc.Client, e girc.Event) { + fmt.Printf("TODO topic params: %#v", e.Params) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c032fe7 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.deuxfleurs.fr/Deuxfleurs/easybridge + +go 1.13 + +require github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..59abef6 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 h1:BS9tqL0OCiOGuy/CYYk2gc33fxqaqh5/rhqMKu4tcYA= +github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7/go.mod h1:liX5MxHPrwgHaKowoLkYGwbXfYABh1jbZ6FpElbGF1I= diff --git a/main.go b/main.go new file mode 100644 index 0000000..47d2cec --- /dev/null +++ b/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "strings" + "time" + "fmt" + "log" + + "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" + "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/irc" +) + +type TmpHandler struct{ + exit chan bool +} + +func (h *TmpHandler) Joined(roomId connector.RoomID) { + fmt.Printf("C Joined: %s\n", roomId) +} + +func (h *TmpHandler) Left(roomId connector.RoomID) { + fmt.Printf("C Joined: %s\n", roomId) +} + +func (h *TmpHandler) UserInfoUpdated(u connector.UserID, i *connector.UserInfo) { + fmt.Printf("C User info: %s => %#v\n", u, i) +} + +func (h *TmpHandler) RoomInfoUpdated(r connector.RoomID, i *connector.RoomInfo) { + fmt.Printf("C Room info: %s => %#v\n", r, i) +} +func (h *TmpHandler) Event(e *connector.Event) { + if e.Type == connector.EVENT_JOIN { + fmt.Printf("C E Join %s %s\n", e.Author, e.Room) + } else if e.Type == connector.EVENT_LEAVE { + fmt.Printf("C E Leave %s %s\n", e.Author, e.Room) + } else if e.Type == connector.EVENT_MESSAGE { + fmt.Printf("C E Message %s %s %s\n", e.Author, e.Room, e.Message) + if strings.Contains(e.Message, "ezbrexit") { + fmt.Printf("we have to exit") + h.exit <- true + } + } else if e.Type == connector.EVENT_ACTION { + fmt.Printf("C E Action %s %s %s\n", e.Author, e.Room, e.Message) + } +} + +func main() { + irc := &irc.IRC{} + h := TmpHandler{ + exit: make(chan bool), + } + irc.SetHandler(&h) + + err := irc.Configure(connector.Configuration{ + "server": "irc.ulminfo.fr", + "port": "6666", + "nick": "ezbr", + }) + if err != nil { + log.Fatalf("Connect: %s", err) + } + + err = irc.Join(connector.RoomID("#ezbrtest@irc.ulminfo.fr")) + if err != nil { + log.Fatalf("Join: %s", err) + } + + time.Sleep(time.Duration(1)*time.Second) + err = irc.Send(&connector.Event{ + Room: connector.RoomID("#ezbrtest@irc.ulminfo.fr"), + Type: connector.EVENT_MESSAGE, + Message: "EZBR TEST", + }) + if err != nil { + log.Fatalf("Send: %s", err) + } + + time.Sleep(time.Duration(1)*time.Second) + err = irc.Send(&connector.Event{ + Recipient: connector.UserID("lx@irc.ulminfo.fr"), + Type: connector.EVENT_MESSAGE, + Message: "EZBR TEST direct message lol", + }) + if err != nil { + log.Fatalf("Send: %s", err) + } + + fmt.Printf("waiting exit signal") + <-h.exit + fmt.Printf("got exit signal") + irc.Close() +}