aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2020-02-16 16:26:55 +0100
committerAlex Auvolat <alex@adnab.me>2020-02-16 16:26:55 +0100
commitec67a610e3062d3e60891332f33ace4ad5a448bd (patch)
treedb4f271270c20419a620b91db4224bfeda083bf2
downloadeasybridge-ec67a610e3062d3e60891332f33ace4ad5a448bd.tar.gz
easybridge-ec67a610e3062d3e60891332f33ace4ad5a448bd.zip
First commit with working stub of IRC bridge
-rw-r--r--.gitignore1
-rw-r--r--Makefile2
-rw-r--r--connector/config.go21
-rw-r--r--connector/connector.go134
-rw-r--r--connector/irc/irc.go255
-rw-r--r--go.mod5
-rw-r--r--go.sum2
-rw-r--r--main.go93
8 files changed, 513 insertions, 0 deletions
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()
+}