IRC gateway

From Rosetta Code
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.
Create an IRC Gateway capable of connecting an IRC server with another IRC server or Chat server

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