Create an IRC Gateway capable of connecting an IRC server with another IRC server or Chat server

IRC gateway is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

Go

Library: go-ircevent


Just a bare-bones gateway.

package main

import (
    "crypto/tls"
    "fmt"
    "github.com/thoj/go-ircevent"
    "log"
    "os"
)

func main() {
    if len(os.Args) != 9 {
        fmt.Println("To use this gateway, you need to pass 8 command line arguments, namely:")
        fmt.Println("  <server1> <channel1> <nick1> <user1> <server2> <channel2> <nick2> <user2>")
        return
    }
    server1, channel1, nick1, user1 := os.Args[1], os.Args[2], os.Args[3], os.Args[4]
    server2, channel2, nick2, user2 := os.Args[5], os.Args[6], os.Args[7], os.Args[8]

    irc1 := irc.IRC(nick1, user1)
    irc1.VerboseCallbackHandler = true
    irc1.Debug = false
    irc1.UseTLS = true
    irc1.TLSConfig = &tls.Config{InsecureSkipVerify: true}

    irc2 := irc.IRC(nick2, user2)
    irc2.VerboseCallbackHandler = true
    irc2.Debug = false
    irc2.UseTLS = true
    irc2.TLSConfig = &tls.Config{InsecureSkipVerify: true}

    irc1.AddCallback("001", func(e *irc.Event) {
        irc1.Join(channel1)
        msg := fmt.Sprintf("<gateway> Hello %s. Please send your first message to %s.", nick1, nick2)
        irc1.Privmsg(nick1, msg)
        log.Println(msg)
    })

    irc1.AddCallback("366", func(e *irc.Event) {})

    irc1.AddCallback("PRIVMSG", func(e *irc.Event) {
        msg := fmt.Sprintf("<%s> %s", nick1, e.Message)
        irc2.Privmsg(nick2, msg)
        log.Println(msg)
    })

    irc2.AddCallback("001", func(e *irc.Event) {
        irc2.Join(channel2)
        msg := fmt.Sprintf("<gateway> Hello %s. Please wait for your first message from %s.", nick2, nick1)
        irc2.Privmsg(nick2, msg)
        log.Println(msg)
    })

    irc2.AddCallback("366", func(e *irc.Event) {})

    irc2.AddCallback("PRIVMSG", func(e *irc.Event) {
        msg := fmt.Sprintf("<%s> %s", nick2, e.Message)
        irc1.Privmsg(nick1, msg)
        log.Println(msg)
    })

    err1 := irc1.Connect(server1)
    if err1 != nil {
        log.Fatal(err1)
    }

    err2 := irc2.Connect(server2)
    if err2 != nil {
        log.Fatal(err2)
    }

    go irc2.Loop()
    irc1.Loop()
}

Phix

For use with Chat_server#Phix

--
-- demo\rosetta\IRC_Gateway.exw
-- ============================
--
-- Run ChatServer first, then this, then ChatClient.exw with bViaGateway set to true.
--
-- Translation between the various IRC protocols left as an exercise for the reader,
-- this is just a simple passthrough service.
--
-- Also this uses a simpler server <--> gateway <--> {clients} model, rather than
-- a more sensible server <-> {gateway1 <-> client1, gateway2 <-> client2} model,
-- probably easily changed, see the {ci,gi} = connection[i] etc comments below.
-- Hence as is this will broadcast eg /nt (name taken) rather than send it to a
-- single specific client, which would obviously be better.
--
without js
constant dl = `Download rosetta\eulibnet\ from http://phix.x10.mx/pmwiki/pmwiki.php?n=Main.Eulibnet`
assert(get_file_type("eulibnet")=FILETYPE_DIRECTORY,dl)
include eulibnet/eulibnet.ew

atom gateway_listconn,
     server_listconn
constant IP = "127.0.0.1",
         server_port = "29029",
         gateway_port = "29030",
         server_address = IP & ":" & server_port,
         gateway_address = IP & ":" & gateway_port,
         timeout = 20,
         MAX_MSG = 550,
         Escape = #1B

sequence connections = {} -- (inbound on the gateway port)
                          -- (should maybe {inbound,outbound})  

procedure message(string msg, sequence args={})
    if length(args) then msg = sprintf(msg,args) end if
    printf(1,"%s\n",{msg})
end procedure

procedure shutDown()
    message("Shutting down euLibnet...")
    for i = 1 to length(connections) do
--DEV/SUG
--      integer {ci,gi} = connections[i]
--      if net_closeconn(ci) then crash("Error closing client connection!") end if
--      if net_closeconn(gi) then crash("Error closing gateway connection!") end if
        if net_closeconn(connections[i]) then crash("Error closing connection!") end if
    end for
    if net_closeconn(gateway_listconn) then crash("Error closing gateway_listconn!") end if
    if net_closeconn(server_listconn) then crash("Error closing server_listconn!") end if
    if net_shutdown() then crash("Error shutting down euLibnet!") end if
end procedure

--DEV to go if {ci,gi} model adopted... probably
procedure sendToAll(string msg)
    -- Send msg to all clients
    for i=1 to length(connections) do
        atom ci = connections[i]
        message("Sending to connection %d",{ci})
        if net_send_rdm(ci, msg) then
            message("Error sending to connection %d",{ci})
        end if
    end for
end procedure

message("Initializing euLibnet...")
if net_init() then crash("Error initializing euLibnet!") end if
message("done.")
message("Initializing driver...")
if net_initdriver(NET_DRIVER_WSOCK_WIN) != 1 then
    crash("Error initializing WinSock driver!")
end if
message("done.")
message("Opening port " & gateway_address & "...")
gateway_listconn = net_openconn(NET_DRIVER_WSOCK_WIN, gateway_address)
if gateway_listconn = NULL then
    crash("Couldn't open connection (gateway already running?)")
end if
message("done.")
if net_listen(gateway_listconn) then
    crash("Error trying to listen to port")
end if
message("Listening on port " & gateway_address)

--DEV some/all probably better as {ci,gi} in connections[i]:
message("Opening server connection...")
server_listconn = net_openconn(NET_DRIVER_WSOCK_WIN, NULL)
if server_listconn = NULL then
    crash("Couldn't open connection.")
end if
message("done.")
message("Attempting to connect to chat server...")
integer ret = net_connect_wait_time(server_listconn, server_address, timeout)
if ret < 0 then
    crash("Error trying to establish connection.")
elsif ret > 0 then
    crash("Timeout trying to establish connection.")
end if
message("done.")

-- main loop (poll until Escape keyed)
while get_key()!=Escape do
    integer conn = net_poll_listen(gateway_listconn)
--  integer ci = net_poll_listen(gateway_listconn), gi
    if conn != NULL then
--DEV make a new gateway connection to the server here...?
--       gi = <as server_listconn above?>
--       connections = append(connections, {ci,gi})
         connections = append(connections, conn)
         message("New connection open from " & net_getpeer(conn))
    end if

    -- Check for messages from clients
    for i=1 to length(connections) do
        integer ci = connections[i]
--      {ci,gi} = connections[i]
        if net_query_rdm(ci) > 0 then
            --Get the message
            sequence msg = net_receive_rdm(ci, MAX_MSG)
            message("received msg \"%s\" of length %d from %d",{msg[2],msg[1],ci})
            if msg[1] < 0 then --Exit on error
                {} = net_ignore_rdm(ci)
                crash("Server error: some data may be lost")
            end if

            msg = msg[2]
            message("Sending to server")
            if net_send_rdm(server_listconn, msg) then
--          if net_send_rdm(gi, msg) then
                crash("Error sending to server")
            end if
        end if
--      <as below but for gi instead of server_listcomm here?>
    end for

    if net_query_rdm(server_listconn) > 0 then --Check for message
--  if net_query_rdm(gi) > 0 then --Check for message
        sequence msg = net_receive_rdm(server_listconn, MAX_MSG)
--      sequence msg = net_receive_rdm(gi, MAX_MSG)
        if msg[1] < 0 then
           message("Error receiving message!")
           {} = net_ignore_rdm(server_listconn)
--         {} = net_ignore_rdm(gi)
        end if
        msg = msg[2]
        message(msg)
        sendToAll(msg)
--DEV maybe instead:
--      if net_send_rdm(ci, msg) then
--          crash("Error sending to clent")
--      end if
    end if

    sleep(1)
end while
shutDown()

--?"done"
--{} = wait_key()

Tcl

This code is called as a complete script, perhaps like this:

./ircgateway.tcl irc://hostA.org/fishing bait irc://hostB.com:6667/haxors botfly
Library: Tcllib (Package: picoirc)
#!/bin/env tclsh8.5
package require picoirc

### Parse script arguments
# URL form: irc://foobar.org/secret
if {$argc != 4} {
    puts stderr "wrong # args: should be \"$argv0 ircA nickA ircB nickB\""
    exit 1
}
lassign $argv url1 nick1 url2 nick2

### How to do the forwarding from one side to the other
proc handle {from to -> state args} {
    upvar #0 conn($from) f conn($to) t chan($to) chan
    switch -exact -- $state {
	"chat" {
	    lassign $args target nick message type
	    if {![string match "*>>*<<*" $message]} {
		picoirc::post $t $chan ">>$nick said<< $message"
	    }
	}
	"traffic" {
	    lassign $args action channel nick newnick
	    switch -exact -- $action {
		"entered" - "left" {
		    picoirc::post $t $chan ">>$nick has $action<<"
		}
	    }
	}
	"close" {
	    exit
	}
    }
}

### Connect and run the event loop
set chan(1) [lindex [picoirc::splituri $url1] 2]
set chan(2) [lindex [picoirc::splituri $url1] 2]
interp alias {} handle1to2 {} handle 1 2
interp alias {} handle2to1 {} handle 2 1
set conn(1) [picoirc::connect handle1to2 $nick1 $url1]
set conn(2) [picoirc::connect handle2to1 $nick2 $url2]
vwait forever

Wren

Translation of: Go
Library: WrenGo
Library: go-ircevent
Library: Wren-dynamic

An embedded application with a Go host so we can use the 'go-ircevent' library.

As it's not possible to access command line arguments directly from Wren when it is being embedded, we instead ask the gateway user to input details of the connections needed.

/* IRC_gateway.wren */

import "./dynamic" for Tuple

var Connection = Tuple.create("Connection", ["server", "channel", "nick", "user"])

foreign class IRC {
    construct new(number, nick, user) {}
    foreign connect(server)
    foreign verboseCallbackHandler=(arg)
    foreign debug=(arg)
    foreign useTLS=(arg)
    foreign configTLS=(arg)
    foreign addCallback(number, code, msg, channel, nick, otherNick)
}

foreign class Reader {
    construct new() {}
    foreign readLine()
}

var reader = Reader.new()
var Connections = List.filled(2, null)
System.print("To use this gateway, please enter the following:\n")
for (i in 0..1) {
    System.print("Details for connection %(i+1):")
    System.write("  Server   : ")
    var server = reader.readLine()
    System.write("  Channel  : ")
    var channel = reader.readLine()
    System.write("  Nickname : ")
    var nick = reader.readLine()
    System.write("  User     : ")
    var user = reader.readLine()
    Connections[i] = Connection.new(server, channel, nick, user)
    System.print()
}

for (i in 0..1) {
    var c = Connections[i]
    var irc = IRC.new(i, c.nick, c.user)
    irc.verboseCallbackHandler = true
    irc.debug = false
    irc.useTLS = true
    irc.configTLS = true
    var otherNick = (i == 0) ? Connections[1].nick : Connections[0].nick
    var msg
    if (i == 0) {
        msg = "<gateway> Hello %(c.nick). Please send your first message to %(otherNick)."
    } else {
        msg = "<gateway> Hello %(c.nick). Please wait for your first message from %(otherNick)."
    }
    irc.addCallback(i, "001",     msg, c.channel, c.nick, otherNick)
    irc.addCallback(i, "366",     "" , c.channel, c.nick, otherNick)
    irc.addCallback(i, "PRIVMSG", "" , c.channel, c.nick, otherNick)
    irc.connect(c.server)
}

We now embed this script in the following Go program and run it from a terminal. To close the gateway just press Ctrl-C.

/* go run IRC_gateway.go */

package main

import (
    "bufio"
    "crypto/tls"
    "fmt"
    wren "github.com/crazyinfin8/WrenGo"
    "github.com/thoj/go-ircevent"
    "log"
    "os"
    "strings"
)

type any = interface{} // not needed if using Go v1.18 or later

var ircObjs [2]*irc.Connection

func newIRC(vm *wren.VM, parameters []any) (any, error) {
    number := int(parameters[1].(float64))
    nick := parameters[2].(string)
    user := parameters[3].(string)
    ircObjs[number] = irc.IRC(nick, user)
    return &ircObjs[number], nil
}

func connect(vm *wren.VM, parameters []any) (any, error) {
    handle := parameters[0].(*wren.ForeignHandle)
    ifc, _ := handle.Get()
    ircObj := *(ifc.(**irc.Connection))
    server := parameters[1].(string)
    err := ircObj.Connect(server)
    if err != nil {
        log.Fatal(err)
    }
    return nil, nil
}

func setVerboseCallbackHandler(vm *wren.VM, parameters []any) (any, error) {
    handle := parameters[0].(*wren.ForeignHandle)
    ifc, _ := handle.Get()
    ircObj := *(ifc.(**irc.Connection))
    value := parameters[1].(bool)
    ircObj.VerboseCallbackHandler = value
    return nil, nil
}

func setDebug(vm *wren.VM, parameters []any) (any, error) {
    handle := parameters[0].(*wren.ForeignHandle)
    ifc, _ := handle.Get()
    ircObj := *(ifc.(**irc.Connection))
    value := parameters[1].(bool)
    ircObj.Debug = value
    return nil, nil
}

func setUseTLS(vm *wren.VM, parameters []any) (any, error) {
    handle := parameters[0].(*wren.ForeignHandle)
    ifc, _ := handle.Get()
    ircObj := *(ifc.(**irc.Connection))
    value := parameters[1].(bool)
    ircObj.UseTLS = value
    return nil, nil
}

func setConfigTLS(vm *wren.VM, parameters []any) (any, error) {
    handle := parameters[0].(*wren.ForeignHandle)
    ifc, _ := handle.Get()
    ircObj := *(ifc.(**irc.Connection))
    value := parameters[1].(bool)
    ircObj.TLSConfig = &tls.Config{InsecureSkipVerify: value}
    return nil, nil
}

func addCallback(vm *wren.VM, parameters []any) (any, error) {
    handle := parameters[0].(*wren.ForeignHandle)
    ifc, _ := handle.Get()
    ircObj := *(ifc.(**irc.Connection))
    number := int(parameters[1].(float64))
    code := parameters[2].(string)
    msg := parameters[3].(string)
    channel := parameters[4].(string)
    nick := parameters[5].(string)
    otherNick := parameters[6].(string)
    if code == "001" {
        ircObj.AddCallback("001", func(e *irc.Event) {
            ircObj.Join(channel)
            ircObj.Privmsg(nick, msg)
            log.Println(msg)
        })
    } else if code == "366" {
        ircObj.AddCallback("366", func(e *irc.Event) {})
    } else if code == "PRIVMSG" {
        ircObj.AddCallback("PRIVMSG", func(e *irc.Event) {
            msg := fmt.Sprintf("<%s> %s", nick, e.Message)
            if number == 0 {
                ircObjs[1].Privmsg(otherNick, msg)
            } else {
                ircObjs[0].Privmsg(otherNick, msg)
            }
            log.Println(msg)
        })
    }
    return nil, nil
}

func newReader(vm *wren.VM, parameters []any) (any, error) {
    reader := bufio.NewReader(os.Stdin)
    return &reader, nil
}

func readLine(vm *wren.VM, parameters []any) (any, error) {
    handle := parameters[0].(*wren.ForeignHandle)
    ifc, _ := handle.Get()
    bufin := *(ifc.(**bufio.Reader))
    s, _ := bufin.ReadString('\n') // includes the delimiter
    return s[:len(s)-1], nil
}

func moduleFn(vm *wren.VM, name string) (string, bool) {
    if name != "meta" && name != "random" && !strings.HasSuffix(name, ".wren") {
        name += ".wren"
    }
    return wren.DefaultModuleLoader(vm, name)
}

func main() {
    cfg := wren.NewConfig()
    cfg.LoadModuleFn = moduleFn
    vm := cfg.NewVM()
    var fileName = "IRC_gateway.wren"
    IRCMethodMap := wren.MethodMap{
        "connect(_)":                 connect,
        "verboseCallbackHandler=(_)": setVerboseCallbackHandler,
        "debug=(_)":                  setDebug,
        "useTLS=(_)":                 setUseTLS,
        "configTLS=(_)":              setConfigTLS,
        "addCallback(_,_,_,_,_,_)":   addCallback,
    }

    readerMethodMap := wren.MethodMap{"readLine()": readLine}

    classMap := wren.ClassMap{
        "IRC":    wren.NewClass(newIRC, nil, IRCMethodMap),
        "Reader": wren.NewClass(newReader, nil, readerMethodMap),
    }

    module := wren.NewModule(classMap)
    vm.SetModule(fileName, module)
    vm.InterpretFile(fileName)
    go ircObjs[1].Loop()
    ircObjs[0].Loop()
    vm.Free()
}