First commit with working stub of IRC bridge
This commit is contained in:
commit
ec67a610e3
8 changed files with 513 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
easybridge
|
2
Makefile
Normal file
2
Makefile
Normal file
|
@ -0,0 +1,2 @@
|
|||
all:
|
||||
go build
|
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