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 }