Added basic IRC library fully written in Fun. (0.41.11)
This commit is contained in:
parent
92bb5d4471
commit
91f4fa3c07
4 changed files with 373 additions and 1 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
project(fun VERSION 0.41.10 LANGUAGES C)
|
project(fun VERSION 0.41.11 LANGUAGES C)
|
||||||
|
|
||||||
set(CMAKE_C_STANDARD 99)
|
set(CMAKE_C_STANDARD 99)
|
||||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
|
||||||
80
examples/net/irc_channel_demo.fun
Normal file
80
examples/net/irc_channel_demo.fun
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
#!/usr/bin/env fun
|
||||||
|
|
||||||
|
/*
|
||||||
|
* IRC channel demo using lib/net/irc.fun
|
||||||
|
*
|
||||||
|
* Connects to an IRC server, registers, joins #funlang, sends a message to
|
||||||
|
* the channel, reads a bit (responding to PINGs), then quits.
|
||||||
|
*
|
||||||
|
* TLS via local tunnel:
|
||||||
|
* - Point host to 127.0.0.1 and port to your local TLS tunnel.
|
||||||
|
* - Set use_tls = 1 (the library still uses plain TCP to the local tunnel).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <net/irc.fun>
|
||||||
|
#include <strings.fun>
|
||||||
|
#include <io/console.fun>
|
||||||
|
#include <utils/datetime.fun>
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
// freenode (plain): host="chat.freenode.net", port=6667, use_tls=0
|
||||||
|
// For TLS via a local tunnel to remote 6697: host="127.0.0.1", port=<local>, use_tls=1
|
||||||
|
host = "chat.freenode.net"
|
||||||
|
port = 6667
|
||||||
|
use_tls = 0
|
||||||
|
|
||||||
|
nick = "FunChannelBot" // Pick an unused nickname
|
||||||
|
user = "funchan"
|
||||||
|
real = "Hello superbrain! Fun IRC Channel Demo! Kisses hanez!"
|
||||||
|
pass = "" // Optional network password (not NickServ)
|
||||||
|
channel = "#fun-lang" // Requested channel
|
||||||
|
|
||||||
|
// Date/time helper
|
||||||
|
dt = DateTime()
|
||||||
|
|
||||||
|
// Helper: read, answer PINGs, and print lines
|
||||||
|
fun handle_lines(c)
|
||||||
|
lines = c.read_lines()
|
||||||
|
i = 0
|
||||||
|
while (i < len(lines))
|
||||||
|
line = lines[i]
|
||||||
|
msg = c.parse_message(line)
|
||||||
|
if (msg["command"] == "PING")
|
||||||
|
t = msg["trailing"]
|
||||||
|
if (len(t) == 0 && len(msg["params"]) > 0)
|
||||||
|
t = msg["params"][0]
|
||||||
|
c.pong(t)
|
||||||
|
print(line)
|
||||||
|
i = i + 1
|
||||||
|
|
||||||
|
mode = " (plain)"
|
||||||
|
if (use_tls == 1)
|
||||||
|
mode = " (TLS via tunnel)"
|
||||||
|
print("Connecting to " + host + ":" + to_string(port) + mode)
|
||||||
|
c = IRCClient(host, port, use_tls)
|
||||||
|
if (!c.connect())
|
||||||
|
print("Failed to connect.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
c.register(nick, user, real, pass)
|
||||||
|
|
||||||
|
// Process greetings and auto-respond to PINGs
|
||||||
|
start = dt.now_ms()
|
||||||
|
while (dt.now_ms() - start < 3000)
|
||||||
|
handle_lines(c)
|
||||||
|
|
||||||
|
// Join #funlang and speak
|
||||||
|
c.join(channel)
|
||||||
|
dt.sleep_ms(500)
|
||||||
|
c.privmsg(channel, real)
|
||||||
|
|
||||||
|
// Read a bit more to display echoes/traffic
|
||||||
|
start2 = dt.now_ms()
|
||||||
|
while (dt.now_ms() - start2 < 2000)
|
||||||
|
handle_lines(c)
|
||||||
|
|
||||||
|
c.quit("bye")
|
||||||
|
dt.sleep_ms(200)
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
print("Done.")
|
||||||
84
examples/net/irc_minimal.fun
Normal file
84
examples/net/irc_minimal.fun
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
#!/usr/bin/env fun
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Minimal example using lib/net/irc.fun
|
||||||
|
*
|
||||||
|
* This demo connects to an IRC server, registers, joins a channel, sends one
|
||||||
|
* message, reads for a short while (responding to PINGs), then quits.
|
||||||
|
*
|
||||||
|
* TLS usage via local tunnel (recommended):
|
||||||
|
* 1) Install stunnel and create a client service (example in repo docs).
|
||||||
|
* 2) Run this script pointing to localhost:<local_tls_port>, use_tls=1.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <net/irc.fun>
|
||||||
|
#include <strings.fun>
|
||||||
|
#include <io/console.fun>
|
||||||
|
#include <utils/datetime.fun>
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
// freenode (plain): host="chat.freenode.net", port=6667, use_tls=0
|
||||||
|
// For TLS via a local tunnel to remote 6697: host="127.0.0.1", port=<local>, use_tls=1
|
||||||
|
host = "chat.freenode.net"
|
||||||
|
port = 6667
|
||||||
|
use_tls = 0
|
||||||
|
|
||||||
|
nick = "FunDemoBot" // Pick an unused nickname
|
||||||
|
user = "fundemo"
|
||||||
|
real = "Fun Demo Bot"
|
||||||
|
pass = "" // Optional network password (not NickServ)
|
||||||
|
channel = "#funlang" // Use a channel you may write to
|
||||||
|
target_nick = "hanez" // For a test private message
|
||||||
|
|
||||||
|
// Date/time helper
|
||||||
|
dt = DateTime()
|
||||||
|
|
||||||
|
// Helper: read, answer PINGs, and print lines
|
||||||
|
fun handle_lines(c)
|
||||||
|
lines = c.read_lines()
|
||||||
|
i = 0
|
||||||
|
while (i < len(lines))
|
||||||
|
line = lines[i]
|
||||||
|
msg = c.parse_message(line)
|
||||||
|
if (msg["command"] == "PING")
|
||||||
|
t = msg["trailing"]
|
||||||
|
if (len(t) == 0 && len(msg["params"]) > 0)
|
||||||
|
t = msg["params"][0]
|
||||||
|
c.pong(t)
|
||||||
|
print(line)
|
||||||
|
i = i + 1
|
||||||
|
|
||||||
|
mode = " (plain)"
|
||||||
|
if (use_tls == 1)
|
||||||
|
mode = " (TLS via tunnel)"
|
||||||
|
print("Connecting to " + host + ":" + to_string(port) + mode)
|
||||||
|
c = IRCClient(host, port, use_tls)
|
||||||
|
if (!c.connect())
|
||||||
|
print("Failed to connect.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
c.register(nick, user, real, pass)
|
||||||
|
|
||||||
|
// Process greetings and auto-respond to PINGs
|
||||||
|
start = dt.now_ms()
|
||||||
|
while (dt.now_ms() - start < 3000)
|
||||||
|
handle_lines(c)
|
||||||
|
|
||||||
|
// Join and speak
|
||||||
|
c.join(channel)
|
||||||
|
dt.sleep_ms(500)
|
||||||
|
c.privmsg(channel, "Hello from Fun!")
|
||||||
|
|
||||||
|
// Send a test message to a nick as well
|
||||||
|
c.privmsg(target_nick, "Hi from Fun example script!")
|
||||||
|
|
||||||
|
// Read a bit more
|
||||||
|
start2 = dt.now_ms()
|
||||||
|
while (dt.now_ms() - start2 < 2000)
|
||||||
|
handle_lines(c)
|
||||||
|
|
||||||
|
c.quit("bye")
|
||||||
|
dt.sleep_ms(200)
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
print("Done.")
|
||||||
208
lib/net/irc.fun
Normal file
208
lib/net/irc.fun
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
/*
|
||||||
|
* Basic IRC client library for Fun
|
||||||
|
*
|
||||||
|
* Implements a class-based client for the IRC protocol suitable for simple
|
||||||
|
* bots or message senders. Designed to work over plain TCP. For TLS, point
|
||||||
|
* the client at a local TLS tunnel (e.g., stunnel) that connects to the
|
||||||
|
* remote IRC TLS endpoint.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <io/socket.fun>
|
||||||
|
#include <strings.fun>
|
||||||
|
|
||||||
|
class IRCClient(string host, number port, number use_tls)
|
||||||
|
|
||||||
|
fun _construct(this, host, port, use_tls)
|
||||||
|
this.host = to_string(host)
|
||||||
|
this.port = to_number(port)
|
||||||
|
this.use_tls = to_number(use_tls) // 0 = plain, 1 = TLS via local tunnel
|
||||||
|
this.nick = ""
|
||||||
|
this.user = ""
|
||||||
|
this.realname = ""
|
||||||
|
this.password = ""
|
||||||
|
this.connected = 0
|
||||||
|
this._buf = "" // line buffer for recv()
|
||||||
|
|
||||||
|
// Under current stdlib, only TcpClient is available. For TLS, connect to
|
||||||
|
// a local tunnel (e.g., stunnel) so we still use plain TCP here.
|
||||||
|
this.cli = TcpClient()
|
||||||
|
|
||||||
|
fun connect(this)
|
||||||
|
ok = this.cli.connect(this.host, this.port)
|
||||||
|
if (ok)
|
||||||
|
this.connected = 1
|
||||||
|
else
|
||||||
|
this.connected = 0
|
||||||
|
return ok
|
||||||
|
|
||||||
|
fun is_connected(this)
|
||||||
|
return this.connected == 1 && this.cli.is_connected()
|
||||||
|
|
||||||
|
fun close(this)
|
||||||
|
if (this.cli != nil)
|
||||||
|
this.cli.close()
|
||||||
|
this.connected = 0
|
||||||
|
return 1
|
||||||
|
|
||||||
|
// Basic RFC 1459 registration; password optional (PASS before NICK/USER)
|
||||||
|
fun register(this, nick, user, realname, password)
|
||||||
|
this.nick = to_string(nick)
|
||||||
|
this.user = to_string(user)
|
||||||
|
this.realname = to_string(realname)
|
||||||
|
this.password = to_string(password)
|
||||||
|
|
||||||
|
if (len(this.password) > 0)
|
||||||
|
this.send_raw("PASS " + this.password)
|
||||||
|
|
||||||
|
this.send_raw("NICK " + this.nick)
|
||||||
|
// USER <username> <mode: 0> <unused: *> :<realname>
|
||||||
|
this.send_raw("USER " + this.user + " 0 * :" + this.realname)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
// Send raw IRC line (no trailing CRLF required by the caller)
|
||||||
|
fun send_raw(this, line)
|
||||||
|
if (!this.is_connected())
|
||||||
|
return -1
|
||||||
|
// Ensure CRLF termination
|
||||||
|
msg = to_string(line)
|
||||||
|
// Avoid dependency on str_ends_with; do it manually
|
||||||
|
need_crlf = 1
|
||||||
|
if (len(msg) >= 2)
|
||||||
|
tail = substr(msg, len(msg) - 2, 2)
|
||||||
|
if (tail == "\r\n")
|
||||||
|
need_crlf = 0
|
||||||
|
if (need_crlf == 1)
|
||||||
|
msg = msg + "\r\n"
|
||||||
|
return this.cli.send(msg)
|
||||||
|
|
||||||
|
fun join(this, channel)
|
||||||
|
return this.send_raw("JOIN " + to_string(channel))
|
||||||
|
|
||||||
|
fun part(this, channel, msg)
|
||||||
|
ch = to_string(channel)
|
||||||
|
m = to_string(msg)
|
||||||
|
if (len(m) > 0)
|
||||||
|
return this.send_raw("PART " + ch + " :" + m)
|
||||||
|
return this.send_raw("PART " + ch)
|
||||||
|
|
||||||
|
fun privmsg(this, target, text)
|
||||||
|
t = to_string(text)
|
||||||
|
return this.send_raw("PRIVMSG " + to_string(target) + " :" + t)
|
||||||
|
|
||||||
|
fun notice(this, target, text)
|
||||||
|
return this.send_raw("NOTICE " + to_string(target) + " :" + to_string(text))
|
||||||
|
|
||||||
|
fun pong(this, token)
|
||||||
|
return this.send_raw("PONG :" + to_string(token))
|
||||||
|
|
||||||
|
fun quit(this, message)
|
||||||
|
m = to_string(message)
|
||||||
|
if (len(m) > 0)
|
||||||
|
this.send_raw("QUIT :" + m)
|
||||||
|
else
|
||||||
|
this.send_raw("QUIT")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
// --- Reading and parsing ---
|
||||||
|
|
||||||
|
// Read any available data and emit complete IRC lines (without CRLF)
|
||||||
|
fun read_lines(this)
|
||||||
|
if (!this.is_connected())
|
||||||
|
return []
|
||||||
|
// Read a chunk (tune size as needed)
|
||||||
|
chunk = this.cli.recv(4096)
|
||||||
|
if (len(chunk) == 0)
|
||||||
|
return []
|
||||||
|
|
||||||
|
this._buf = this._buf + chunk
|
||||||
|
out = []
|
||||||
|
// Split on CRLF (IRC standard), also tolerate lone LF
|
||||||
|
while true
|
||||||
|
crlf = find(this._buf, "\r\n")
|
||||||
|
nl = find(this._buf, "\n")
|
||||||
|
sep = -1
|
||||||
|
seplen = 0
|
||||||
|
if (crlf >= 0)
|
||||||
|
sep = crlf
|
||||||
|
seplen = 2
|
||||||
|
else
|
||||||
|
if (nl >= 0)
|
||||||
|
sep = nl
|
||||||
|
seplen = 1
|
||||||
|
|
||||||
|
if (sep < 0)
|
||||||
|
break
|
||||||
|
|
||||||
|
line = substr(this._buf, 0, sep)
|
||||||
|
this._buf = substr(this._buf, sep + seplen, len(this._buf) - sep - seplen)
|
||||||
|
// Strip any trailing CR or LF just in case
|
||||||
|
line = str_trim(line)
|
||||||
|
if (len(line) > 0)
|
||||||
|
push(out, line)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
// Simple IRC message parser -> Map { prefix, command, params (Array), trailing }
|
||||||
|
fun parse_message(this, line)
|
||||||
|
s = to_string(line)
|
||||||
|
n = len(s)
|
||||||
|
|
||||||
|
prefix = ""
|
||||||
|
if (n > 0 && substr(s, 0, 1) == ":")
|
||||||
|
sp = find(s, " ")
|
||||||
|
if (sp > 1)
|
||||||
|
prefix = substr(s, 1, sp - 1)
|
||||||
|
s = substr(s, sp + 1, len(s) - sp - 1)
|
||||||
|
|
||||||
|
// Command up to first space or EOL
|
||||||
|
sp2 = find(s, " ")
|
||||||
|
command = ""
|
||||||
|
rest = ""
|
||||||
|
if (sp2 >= 0)
|
||||||
|
command = substr(s, 0, sp2)
|
||||||
|
rest = str_trim(substr(s, sp2 + 1, len(s) - sp2 - 1))
|
||||||
|
else
|
||||||
|
command = s
|
||||||
|
rest = ""
|
||||||
|
|
||||||
|
// Params until a " :trailing" token; trailing consumes remainder
|
||||||
|
params = []
|
||||||
|
trailing = ""
|
||||||
|
while len(rest) > 0
|
||||||
|
if (substr(rest, 0, 1) == ":")
|
||||||
|
trailing = substr(rest, 1, len(rest) - 1)
|
||||||
|
break
|
||||||
|
sp = find(rest, " ")
|
||||||
|
if (sp < 0)
|
||||||
|
push(params, rest)
|
||||||
|
rest = ""
|
||||||
|
else
|
||||||
|
push(params, substr(rest, 0, sp))
|
||||||
|
rest = str_trim(substr(rest, sp + 1, len(rest) - sp - 1))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"prefix": prefix,
|
||||||
|
"command": str_to_upper(command),
|
||||||
|
"params": params,
|
||||||
|
"trailing": trailing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience: process incoming messages by calling a callback for each line
|
||||||
|
// callback(line, msgMap) where msgMap is output of parse_message
|
||||||
|
fun pump(this, callback)
|
||||||
|
lines = this.read_lines()
|
||||||
|
i = 0
|
||||||
|
while (i < len(lines))
|
||||||
|
ln = lines[i]
|
||||||
|
msg = this.parse_message(ln)
|
||||||
|
// Auto-reply to PINGs
|
||||||
|
if (msg["command"] == "PING")
|
||||||
|
t = msg["trailing"]
|
||||||
|
if (len(t) == 0 && len(msg["params"]) > 0)
|
||||||
|
t = msg["params"][0]
|
||||||
|
this.pong(t)
|
||||||
|
// Invoke user callback
|
||||||
|
if (typeof(callback) == "Function")
|
||||||
|
callback(ln, msg)
|
||||||
|
i = i + 1
|
||||||
|
return len(lines)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue