First commit with working stub of IRC bridge
This commit is contained in:
commit
ec67a610e3
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
easybridge
|
21
connector/config.go
Normal file
21
connector/config.go
Normal 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
134
connector/connector.go
Normal 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
255
connector/irc/irc.go
Normal 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
5
go.mod
Normal 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
2
go.sum
Normal 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
93
main.go
Normal 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()
|
||||||
|
}
|
Loading…
Reference in a new issue