First commit with working stub of IRC bridge

This commit is contained in:
Alex 2020-02-16 16:26:55 +01:00
commit ec67a610e3
8 changed files with 513 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
easybridge

2
Makefile Normal file
View file

@ -0,0 +1,2 @@
all:
go build

21
connector/config.go Normal file
View file

@ -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
}

134
connector/connector.go Normal file
View file

@ -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
}

255
connector/irc/irc.go Normal file
View file

@ -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)
}

5
go.mod Normal file
View file

@ -0,0 +1,5 @@
module git.deuxfleurs.fr/Deuxfleurs/easybridge
go 1.13
require github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7

2
go.sum Normal file
View file

@ -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=

93
main.go Normal file
View file

@ -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()
}