module irc
import net
import io
import regex
import time
import db
import chat
import util
const (
irc_msg_regex = r'^([^ ]+) ([^ ]+)( :?([^ ]+)( :?(.*))?)?'
irc_extra_regex = r'(\S*)(\s+:?([^:]+))?$'
)
struct IrcActor {
pub:
out chan chat.Say
cin chan Payload
pub mut:
networks Networks
puppets Puppets
}
type Payload = Connect | Disconnect | Joined | NickInUse | PrivMsg
struct NickInUse {
pub mut:
puppet &Puppet
}
pub struct Connect {
pub:
puppet &Puppet
}
struct Disconnect {
pub mut:
puppet &Puppet
}
struct Joined {
pub:
puppet &Puppet
channel string
}
struct PrivMsg {
pub:
channel string
puppet Puppet
nick string
pub mut:
message string
}
pub enum ConnState {
connected
disconnected
}
[heap]
pub struct Network {
pub mut:
name string
hostname string
nick string
}
pub struct Channels {
pub mut:
channels []&Channel
}
[heap]
pub struct Channel {
pub mut:
name string
joined bool
}
struct Networks {
pub mut:
networks []&Network
}
struct Puppets {
pub mut:
puppets []&Puppet
}
type SockOrNone = net.TcpConn | util.None
[heap]
struct Puppet {
pub mut:
nick string
state ConnState = ConnState.disconnected
sock SockOrNone
channels Channels
stop bool
network &Network
}
pub fn setup() &IrcActor {
actor := &IrcActor{}
return actor
}
pub enum SayReturn {
good
network_not_found
user_not_found
error
}
pub fn (mut self IrcActor) say(network string, nick string, room string, message string) SayReturn {
mut ircnet := self.networks.by_name(network) or {
println('irc.say() network "$network" not found. (msg was: $message)')
return .network_not_found
}
mut puppet := self.puppets.by_net_nick(ircnet, nick) or {
println('irc.say() puppet nick "$nick" not found in ${network}. (msg was: $message)')
return .user_not_found
}
cmd := 'PRIVMSG $room :$message'
puppet.write(cmd)
return .good
}
pub fn network_from_db(cols []string) &Network {
return &Network{
name: cols[0]
hostname: cols[1]
nick: cols[2]
}
}
pub fn network_to_db(ircnet Network) []db.SqlValue {
mut ircrow := []db.SqlValue{}
ircrow << db.SqlValue{
name: 'hostname'
value: db.SqlType(ircnet.hostname)
}
ircrow << db.SqlValue{
name: 'netname'
value: db.SqlType(ircnet.name)
}
ircrow << db.SqlValue{
name: 'nick'
value: db.SqlType(ircnet.nick)
}
return ircrow
}
pub fn (self Network) str() string {
mut summary := ''
summary = '$self.name/$self.hostname'
return summary
}
pub fn (mut self Networks) by_hostname_idx(hostname string) int {
for idx, ircnet in self.networks {
// vbug if net == ircnet
if hostname == ircnet.hostname {
return idx
}
}
println('error! by_hostname_idx not found $hostname in $self')
return -1
}
pub fn (mut self Networks) by_hostname(addr string) ?&Network {
for mut ircnet in self.networks {
mut p_ircnet := *ircnet // vbug
if p_ircnet.hostname == addr {
return p_ircnet
}
}
println('irc.Networks.by_hostname $addr not found in $self.networks')
return error('server not found')
}
pub fn (mut self Puppet) add_channel(name string) {
if _ := self.find_channel(name) {
} else {
self.channels.channels << &Channel{
name: name
joined: true
}
}
}
pub fn (self Puppet) find_channel(channel_name string) ?&Channel {
for channel in self.channels.channels {
if channel.name == channel_name {
return channel
}
}
return error('not found')
}
pub fn (mut self IrcActor) connect_all() {
for mut ircnet in self.networks.networks {
mut n_ircnet := *ircnet // vbug
ghost := self.puppets.by_net_nick(n_ircnet, n_ircnet.nick) or {
mut n_ghost := self.new_ghost(mut n_ircnet, n_ircnet.nick)
self.puppets.add(mut n_ghost)
}
mut g_ghost := *ghost // vbug
println('irc.connect_all $n_ircnet $g_ghost.nick')
if g_ghost.state == ConnState.disconnected {
g_ghost.dial()
}
}
}
pub fn dial(hostname string, nick string) SockOrNone {
mut host := hostname
if !host.contains(':') {
host = host + ':6667'
}
println('irc.dial() connecting $host $nick')
mut sock := net.dial_tcp(host) or { return SockOrNone(util.None{}) }
sock.set_read_timeout(5 * time.minute)
return SockOrNone(sock)
}
pub fn (mut self Puppet) dial() {
println('$self.nick dial($self.network.hostname, $self.nick)')
if self.state == .connected {
println('$self.nick dial: already connected.')
} else {
self.sock = dial(self.network.hostname, self.nick)
if self.sock is net.TcpConn {
println('$self.nick dial is connected')
self.state = .connected
} else {
println('$self.nick dial failed')
}
}
}
pub fn (mut self Puppet) join(channel string) {
println('$self.nick joining $channel')
cmd := 'JOIN $channel'
self.write(cmd)
}
pub fn (self IrcActor) find_ghost_idx(nick string) int {
for idx, ghost in self.puppets.puppets {
if ghost.nick == nick {
return idx
}
}
return -1
}
pub fn (mut self IrcActor) new_ghost(mut ircnet Network, nick string) &Puppet {
mut puppet := &Puppet{
nick: nick
network: ircnet
}
puppet.dial()
if puppet.sock is net.TcpConn {
go self.comm(mut puppet)
puppet.signin()
} else {
println('WARNING: puppet for $nick added, sock connection failed')
}
return puppet
}
pub fn (mut self Puppet) signin() {
nick_cmd := 'nick $self.nick'
self.write(nick_cmd)
user_cmd := 'user vbridge b c :full name'
self.write(user_cmd)
}
pub fn (mut self Puppets) hangup(ircnet &Network) {
for mut puppet in self.puppets {
mut p_pup := *puppet
if p_pup.network.name == ircnet.name {
p_pup.hangup()
}
}
}
pub fn (self &IrcActor) comm(mut puppet Puppet) {
if mut puppet.sock is net.TcpConn {
println('$puppet.nick comm() started')
mut reader := io.new_buffered_reader(reader: puppet.sock)
for {
if line := reader.read_line() {
_ := self.proto(line, mut puppet)
} else {
println('$puppet.network $puppet.nick comm() TCP closed $err')
puppet.hangup()
self.cin <- Payload(Disconnect{
puppet: puppet
})
break
}
if puppet.stop {
break
}
}
} else {
println('WARNING: $puppet.nick comm() called with missing socket')
}
}
pub fn (self &IrcActor) proto(line string, mut puppet Puppet) string {
// println(line)
parts := parse(line)
if parts.len > 0 {
word := if parts.len == 2 {
parts[0]
} else {
if parts.len > 2 {
parts[1]
} else {
println('irc.proto parse err $parts')
'E_PARSEERR'
}
}
match word {
'001' {
puppet.nick = parts[3] // nick confirmed
}
'002' {}
'003' {}
'004' {}
'005' {
capabilities := capabilities_decode(parts[2])
if netname := capabilities['NETWORK'] {
println('$puppet.network.hostname is part of network $netname')
puppet.network.name = netname
}
}
'250' {
// highest connection count
}
'251' {}
'252' {}
'253' {}
'254' {}
'255' {}
'265' {}
'266' {}
'332' {
// room topic
}
'333' {
// room topic set at
}
'353' {
// room nick list
}
'366' {
// end of nicks
}
'372' {
// drop motd
}
'376' {
// end of motd
println('irc.proto 376 end of motd - Connect $puppet.network.name $puppet.nick')
puppet.state = .connected
self.cin <- Payload(Connect{
puppet: puppet
})
}
'433' {
println('$puppet irc.proto 433 nick $puppet.nick already in use! aborting connection')
self.cin <- Payload(NickInUse{
puppet: puppet
})
}
'NICK' {
//[':donpdonp|z_!~donp@1.2.3.4', 'NICK', ' :donpdonp|z', 'donpdonp|z']
println(parts) // debug
nick_parts := nick_parse(parts[0][1..])
if nick_parts[0] == puppet.nick {
println('NICK changing puppet $puppet.nick to ${parts[3]}')
puppet.nick = parts[3]
} else {
println('$puppet.nick heard NICK change for ${nick_parts[0]} -> ${parts[3]}. ignoring')
}
}
'NOTICE' {}
'JOIN' {
println('sock ${ptr_str(puppet.sock)} $line')
//:donp|m!~a@64.62.134.149 JOIN #roomy-room
nick_parts := nick_parse(parts[0][1..])
channel := parts[3]
println('$puppet.network $puppet.nick puppet ${ptr_str(puppet)}: ${nick_parts[0]} JOINed $channel')
if nick_parts[0] == puppet.nick {
puppet.add_channel(channel)
self.cin <- Payload(Joined{
puppet: puppet
channel: channel
})
}
}
'PING' {
reply := 'PONG ${parts[1]}'
puppet.write(reply)
}
'PRIVMSG' {
mut privmsg := PrivMsg{
channel: parts[3]
puppet: puppet
nick: parts[0][1..] // remove : from protocol
message: ''
}
if ctcp := util.ctcp_decode(parts[5]) {
ctcp_parts := ctcp.split(' ')
match ctcp_parts[0] {
'VERSION' {}
'ACTION' {
privmsg.message = parts[5] // keep ctcp protocol
self.cin <- Payload(privmsg)
}
else {
privmsg.message = 'unknown CTCP: ' + ctcp
self.cin <- Payload(privmsg)
}
}
} else {
privmsg.message = parts[5]
self.cin <- Payload(privmsg)
}
}
'MODE' {
//:tbridge MODE tbridge :+i
}
else {
println('$word $parts')
}
}
}
return ''
}
pub fn nick_parse(full_nick string) []string {
// donp|m!~a@64.62.134.149
mut parts := []string{}
parts << full_nick.before('!')
return parts
}
pub fn parse(line string) []string {
mut parts := []string{}
mut re := regex.regex_opt(irc.irc_msg_regex) or { panic(err) }
re.match_string(line.trim_suffix('\n'))
for g_index := 0; g_index < re.group_count; g_index++ {
start, end := re.get_group_bounds_by_id(g_index)
if start >= 0 {
parts << line[start..end]
}
}
return parts
}
pub fn (self &IrcActor) is_room(room string) bool {
room_match := r'^#[-A-Za-z0-9_]+$'
mut re := regex.regex_opt(room_match) or { panic('regex fail') }
start, _ := re.match_string(room)
println('irc:is_room $room $room_match $start')
return start > -1
}
pub fn (self PrivMsg) str() string {
return '$self.puppet.network $self.puppet.nick: $self.channel $self.nick $self.message'
}
pub fn (self Channels) find_by_name(name string) ?&Channel {
for channel in self.channels {
println('irc.find_by_name [mtx] $name == [irc] $channel.name')
if channel.name == name {
return channel
}
}
return error('not found')
}
pub fn (self &IrcActor) find_channel_by_name(nick string, channel_name string) ?&Channel {
if ghost := self.puppets.by_nick(nick) {
if channel := ghost.channels.find_by_name(channel_name) {
return channel
} else {
println('irc.find_channel_by_name() $ghost.nick ghost ${ptr_str(ghost)} has no $channel_name')
dump(ghost.channels)
}
}
return error('not found')
}
pub fn (self Puppets) by_network(netname string) []&Puppet {
mut winners := []&Puppet{}
for g in self.puppets {
if g.network.name == netname {
winners << g
}
}
return winners
}
pub fn (self Puppets) by_nick(nick string) ?&Puppet {
for mut puppet in self.puppets {
p_pup := *puppet
if p_pup.nick == nick {
println("irc.Puppets.by_nick(\"$nick\") found ghost.nick ${ptr_str(p_pup.nick)} \"$p_pup.nick\" ")
return p_pup
}
}
return error('not found')
}
pub fn (mut self Networks) add(network &Network) &Network {
self.networks << network
return self.networks.last() // network was copied. return copy.
}
pub fn (mut self Networks) by_name(name string) ?&Network {
for mut puppet in self.networks {
mut p_net := *puppet
if p_net.name == name {
return p_net
}
}
return error('not found')
}
pub fn (self Channel) str() string {
return '$self.name'
}
pub fn (self Puppets) by_net_nick(ircnet Network, nick string) ?&Puppet {
for g in self.puppets {
if g.nick == nick && g.network.name == ircnet.name {
println("irc.Puppets.by_net_nick(\"$ircnet.name\" \"$nick\") found ghost.nick ${ptr_str(g.nick)} \"$g.nick\" ")
return g
}
}
return error('not found')
}
pub fn (mut self Puppet) nick(nick string) {
self.write('NICK $nick')
}
pub fn (mut self Puppet) write(msg string) {
if mut self.sock is net.TcpConn {
println('$self.network $self.nick: $msg')
self.sock.write_string(msg + '\n') or {
println('$self.network $self.nick: socket write error $err')
self.hangup()
}
} else {
println('NO SOCK: $self.network $self.nick: dropped $msg')
}
}
pub fn (mut self Puppet) hangup() {
println('$self.nick hangup()')
if mut self.sock is net.TcpConn {
println('$self.nick sock.close()')
self.sock.close() or { println('$self.nick sock.close $err') }
}
self.state = ConnState.disconnected
}
pub fn (self Puppet) str() string {
return '${ptr_str(self)} nick:$self.nick sock:${ptr_str(self.sock)} conn: $self.state channels:$self.channels'
}
pub fn (mut self Puppets) add(mut puppet Puppet) &Puppet {
self.puppets << puppet
return self.puppets.last() // puppet was copied. return copy.
}
pub fn capabilities_decode(capstr string) map[string]string {
parts := capstr.split(' ')
mut cap := map[string]string{}
for part in parts {
cap_parts := part.split('=')
if cap_parts.len == 2 {
key := cap_parts[0]
value := cap_parts[1]
cap[key] = value
}
}
return cap
}