├── src ├── msg │ ├── irc.v │ └── matrix.v ├── v.mod ├── matrix │ ├── message.v │ ├── matrix_test.v │ ├── invite_pm.json │ ├── invite_room.json │ └── matrix.v ├── irc │ ├── message.v │ ├── listen.v │ ├── irc_test.v │ └── irc.v ├── app_test.v ├── debug │ └── debug.v ├── util │ ├── util.v │ └── util_test.v ├── setup │ └── setup.v ├── bridge │ └── bridge.v ├── cli.v ├── rpc │ └── rpc.v ├── appsvc │ └── appsvc.v ├── db │ └── db.v ├── chat │ └── chat.v └── app.v ├── .gitignore ├── README ├── Makefile ├── tilikuv.notes └── pijul2git /src/msg/irc.v: -------------------------------------------------------------------------------- 1 | module msg 2 | 3 | pub struct IrcMsg { 4 | } 5 | -------------------------------------------------------------------------------- /src/msg/matrix.v: -------------------------------------------------------------------------------- 1 | module msg 2 | 3 | pub struct MatrixMsg { 4 | } 5 | -------------------------------------------------------------------------------- /src/v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'ircbr' 3 | description: '' 4 | version: '0.1.0' 5 | dependencies: [] 6 | } 7 | -------------------------------------------------------------------------------- /src/matrix/message.v: -------------------------------------------------------------------------------- 1 | module matrix 2 | 3 | pub struct Message { 4 | pub: 5 | room string 6 | user string 7 | message string 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | registration.yaml 2 | /*.v 3 | /.ignore 4 | /.pijul 5 | /out.sh 6 | /log 7 | /bin 8 | /config.json 9 | /db.sqlite 10 | src/cache/ 11 | -------------------------------------------------------------------------------- /src/irc/message.v: -------------------------------------------------------------------------------- 1 | module irc 2 | 3 | pub struct Message { 4 | pub: 5 | network string 6 | nick string 7 | channel string 8 | message string 9 | } 10 | -------------------------------------------------------------------------------- /src/irc/listen.v: -------------------------------------------------------------------------------- 1 | module irc 2 | 3 | pub fn (mut self IrcActor) listen() { 4 | for { 5 | select { 6 | say := <-self.out { 7 | println('irc listen say<-self.out: $say') 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | vbridge - matrix <-> irc chat bridge 2 | 3 | # setup 4 | 5 | # establish a bridge 6 | 7 | #TODO 8 | * timestamp entries on receipt, so replay can include the correct time 9 | * replay algo: if post success, try the next one immediately 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: run format 2 | 3 | bin/tilikuv: src/**/*.v 4 | v -keepc -showcc -o bin/tilikuv src/app.v 5 | # v -keepc -showcc -o bin/cli src/cli.v 6 | bin: 7 | mkdir bin 8 | run: bin/tilikuv 9 | ./bin/tilikuv 10 | format: 11 | v fmt -w src 12 | push: 13 | pijul push --all 14 | test: 15 | v test . 16 | 17 | -------------------------------------------------------------------------------- /tilikuv.notes: -------------------------------------------------------------------------------- 1 | !irc join #pdxsoz 2 | !bridge add pdxsoz:donp.org #pdxsoz 3 | 4 | !irc join #pdxsoz #pdxsoz:donp.org 5 | !irc join channel:#pdxboz 6 | 7 | !bridge #poif:Libera.chat 8 | 9 | !join #abc 10 | joining #abc on Libera.chat 11 | creating #abc:donp.org 12 | bridging #abc:Libera.chat to #abc:donp.org 13 | joining #abc:donp.org 14 | 15 | -------------------------------------------------------------------------------- /src/app_test.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | fn test_regex_name_match() { 4 | rex := '(.*)|mv' 5 | if name := regex_name_match('(.*)|mv', 'donp|mv') { 6 | assert name == 'donp' 7 | } else { 8 | assert false 9 | } 10 | if _ := regex_name_match('(.*)|mv', 'donpdonp') { 11 | assert false 12 | } else { 13 | assert true 14 | } 15 | } 16 | 17 | fn test_regex_self_replace() { 18 | assert regex_self_replace('(.*)|mv', 'donp') == 'donp|mv' 19 | } 20 | -------------------------------------------------------------------------------- /src/debug/debug.v: -------------------------------------------------------------------------------- 1 | module debug 2 | 3 | pub fn debug(a []&T) { 4 | println('DUMP arr: ${ptr_str(a)} ${typeof(a).name}') 5 | for b in a { 6 | println('DUMP: for b in a{} b => ${ptr_str(b)} ${typeof(b).name}') 7 | } 8 | for idx, _ in a { 9 | b := a[idx] 10 | println('DUMP: for idx, _ in a {} b := a[idx] => ${ptr_str(b)} ${typeof(b).name}') 11 | } 12 | for idx, _ in a { 13 | b := &a[idx] 14 | println('DUMP: for idx, _ in a {} b := &a[idx] => ${ptr_str(b)} ${typeof(b).name}') 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/util/util.v: -------------------------------------------------------------------------------- 1 | module util 2 | 3 | type StringOrNone = None | string 4 | 5 | pub struct None {} 6 | 7 | pub fn (self None) str() string { 8 | return '-none-' 9 | } 10 | 11 | pub fn ctcp_decode(msg string) ?string { 12 | // vlang regex doesnt support \x00 : r'^\x01.*$' 13 | if msg[0] == 1 { 14 | return msg[1..msg.len - 1] 15 | } else { 16 | return error('not a CTCP command') 17 | } 18 | } 19 | 20 | pub fn ctcp_encode(verb string, msg string) string { 21 | soh := byte(1).ascii_str() 22 | return soh + verb + ' ' + msg + soh 23 | } 24 | -------------------------------------------------------------------------------- /pijul2git: -------------------------------------------------------------------------------- 1 | AWK='$0 ~ /Date/ {DATE = $2"T"$3; getline; getline; gsub(/^ +/, "", $0); printf "echo '\''%s %s'\'' >> CHANGELOG ; GIT_AUTHOR_DATE=%s GIT_COMMITTER_DATE=%s git commit -a -m \"%s\"\n", DATE, $0, DATE, DATE, $0 }' 2 | #pijul log|awk -e "$AWK" > out.sh 3 | pijul log|mawk "$AWK" > out.sh 4 | rm -rf .git 5 | git init 6 | git remote add github git@github.com:donpdonp/vbridge.git 7 | git add . 8 | touch CHANGELOG 9 | git add CHANGELOG 10 | . out.sh 11 | rm out.sh 12 | sort -r CHANGELOG | uniq > .changelog 13 | mv .changelog CHANGELOG 14 | git add CHANGELOG 15 | git commit -m "CHANGELOG fixup" 16 | echo DONE 17 | git remote -v 18 | echo $ git push -f github main 19 | -------------------------------------------------------------------------------- /src/util/util_test.v: -------------------------------------------------------------------------------- 1 | module util 2 | 3 | struct Thing { 4 | name StringOrNone 5 | } 6 | 7 | fn test_string_or_none_string() { 8 | thing := Thing{ 9 | name: StringOrNone('bob') 10 | } 11 | println(thing) 12 | assert thing.name is string 13 | } 14 | 15 | fn test_string_or_none_none() { 16 | thing := Thing{ 17 | name: StringOrNone(None{}) 18 | } 19 | println(thing) 20 | assert thing.name is None 21 | } 22 | 23 | fn test_is_ctcp() { 24 | verb := 'ACTION' 25 | msg := 'hop' 26 | ctcp_msg := '\1' + '$verb $msg' + '\1' 27 | 28 | assert ctcp_encode(verb, msg) == ctcp_msg 29 | 30 | if code := ctcp_decode(ctcp_msg) { 31 | assert code == '$verb $msg' 32 | } else { 33 | assert false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/setup/setup.v: -------------------------------------------------------------------------------- 1 | module setup 2 | 3 | import os 4 | import json 5 | 6 | pub struct Config { 7 | pub: 8 | matrix_host string = 'homeserver.example' 9 | matrix_owner string 10 | matrix_regex string 11 | irc_regex string 12 | as_token string = 'fixme' 13 | as_port string = '127.0.0.1:9010' 14 | rpc_port string = '127.0.0.1:9011' 15 | admin_room string 16 | } 17 | 18 | pub fn config() Config { 19 | if json_str := os.read_file('config.json') { 20 | cfg := json.decode(Config, json_str) or { 21 | println('config.json is invalid json') 22 | exit(1) 23 | } 24 | if cfg.matrix_host.len == 0 { 25 | println('config.json: matrix_host setting is empty') 26 | exit(1) 27 | } 28 | return cfg 29 | } else { 30 | json_str := json.encode(Config{}) 31 | os.write_file('config.json', json_str) or { 32 | println('cannot write config.json: $err') 33 | exit(1) 34 | } 35 | println('config.json created. edit this file then run again') 36 | exit(1) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/bridge/bridge.v: -------------------------------------------------------------------------------- 1 | module bridge 2 | 3 | import db 4 | 5 | pub struct Bridge { 6 | left string 7 | right string 8 | } 9 | 10 | pub fn (self Bridge) to_db() []db.SqlValue { 11 | mut parts := []db.SqlValue{} 12 | parts << &db.SqlValue{ 13 | name: 'left' 14 | value: db.SqlType(self.left) 15 | } 16 | parts << &db.SqlValue{ 17 | name: 'right' 18 | value: db.SqlType(self.right) 19 | } 20 | return parts 21 | } 22 | 23 | pub fn matching_room(lr string, mut db db.Db) ?string { 24 | lefts := db.select_by_field('bridge_rooms', 'left', lr) 25 | rights := db.select_by_field('bridge_rooms', 'right', lr) 26 | if lefts.len > 0 { 27 | println('bridge.matching room found left $lr -> right ${lefts[0][1]}') 28 | return lefts[0][1] // pick right 29 | } else if rights.len > 0 { 30 | println('bridge.matching room found right $lr -> left ${rights[0][0]}') 31 | return rights[0][0] // pick left 32 | } 33 | return error('no bridge found for $lr') 34 | } 35 | 36 | pub fn from_db(rows []string) &Bridge { 37 | return &Bridge{ 38 | left: rows[0] 39 | right: rows[1] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/matrix/matrix_test.v: -------------------------------------------------------------------------------- 1 | module matrix 2 | 3 | import setup 4 | import x.json2 5 | import os 6 | 7 | fn test_is_room() { 8 | config := setup.Config{} 9 | m := init(config) 10 | assert m.is_room('#roomname:server.com') 11 | assert m.is_room('!roomid:server.com') 12 | assert m.is_room('#ircroom') == false 13 | } 14 | 15 | fn test_split() { 16 | parts := split('@user:server.com') 17 | assert parts[0] == '@' 18 | assert parts[1] == 'user' 19 | assert parts[2] == 'server.com' 20 | } 21 | 22 | fn test_invite_by() { 23 | config := setup.Config{} 24 | m := init(config) 25 | if json_str := os.read_file('src/matrix/invite_room.json') { 26 | matrix_invite := json2.raw_decode(json_str) or { panic('') }.as_map() 27 | assert m.invited_by(matrix_invite) == '#roomy-room:donp.org' 28 | } else { 29 | assert false 30 | } 31 | if json_str := os.read_file('src/matrix/invite_pm.json') { 32 | matrix_invite := json2.raw_decode(json_str) or { panic('') }.as_map() 33 | assert m.invited_by(matrix_invite) == '@donp:donp.org' 34 | } else { 35 | assert false 36 | } 37 | } 38 | 39 | fn test_escape_char() { 40 | esc := '"\u001b[36mX"' 41 | assert esc.len == 8 42 | jstr := json2.raw_decode(esc) or { panic('') }.str() 43 | assert jstr.len == 6 44 | } 45 | 46 | fn test_mxn_split() { 47 | mxc_url := 'mxc://donp.org/DBKlXYNItaxXzLDEgJwNdKB' 48 | server, id := mxc_split(mxc_url) 49 | assert server == 'donp.org' 50 | assert id == 'DBKlXYNItaxXzLDEgJwNdKB' 51 | } 52 | -------------------------------------------------------------------------------- /src/irc/irc_test.v: -------------------------------------------------------------------------------- 1 | module irc 2 | 3 | fn test_parse_ping() { 4 | irc_msg := 'PING ircbob' 5 | parts := parse(irc_msg) 6 | assert parts.len == 2 7 | } 8 | 9 | fn test_parse_251() { 10 | irc_msg := ':oragono.test 251 ircvbridge :There are 0 users and 1 invisible on 1 server(s)' 11 | parts := parse(irc_msg) 12 | println(parts) 13 | assert parts.len == 6 14 | } 15 | 16 | fn test_parse_privmsg() { 17 | irc_msg := ':Tracey!me@68.178.52.73 PRIVMSG #game1 :ascii escape \u001b[36mcolor' //"\e[COLORmSample Text\e[0m" 18 | parts := parse(irc_msg) 19 | assert parts.len == 6 20 | println(parts[5]) 21 | } 22 | 23 | fn test_is_room() { 24 | ircm := setup() 25 | assert ircm.is_room('#room') 26 | assert ircm.is_room('#roomy-room') 27 | assert ircm.is_room('#roomy-room:donp.org') == false 28 | assert ircm.is_room('!FMIqCsoGJDbtjiBptb:donp.org') == false 29 | } 30 | 31 | fn test_dial() { 32 | mut ircm := setup() 33 | hostname := 'google.com:443' 34 | dial(hostname, 'nick') 35 | assert true 36 | } 37 | 38 | fn test_capabilities_decode() { 39 | //:molybdenum.libera.chat 005 abcdef WHOX KNOCK MONITOR=100 ETRACE FNC SAFELIST ELIST=CTU CALLERID=g CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server 40 | //:molybdenum.libera.chat 005 abcdef CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=Libera.Chat STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server 41 | cap := capabilities_decode('KEY=VALUE') 42 | assert cap['KEY'] == 'VALUE' 43 | } 44 | -------------------------------------------------------------------------------- /src/matrix/invite_pm.json: -------------------------------------------------------------------------------- 1 | { 2 | "age": 378, 3 | "content": { 4 | "displayname": "vbridge", 5 | "membership": "invite" 6 | }, 7 | "event_id": "$z8O_L9KUgSghwnHVs1E7npXF32uUQD92mP7WXXKw06s", 8 | "invite_room_state": [ 9 | { 10 | "content": { 11 | "avatar_url": "mxc://donp.org/xWRPSLhFwLU jvaetqzSvPjnp", 12 | "displayname": "donpdonp", 13 | "membership": "join" 14 | }, 15 | "sender": "@donp:donp.org", 16 | "state_key": "@donp:donp.org", 17 | "type": "m.room.member" 18 | }, 19 | { 20 | "content": { 21 | "join_rule": "invite" 22 | }, 23 | "sender": "@donp:donp.org", 24 | "state_key": " ", 25 | "type": "m.room.join_rules" 26 | } 27 | ], 28 | "origin_server_ts": 1615489138691, 29 | "room_id": "!RbtjkPtlKiEdaNkjkl:donp.org", 30 | "sender": "@donp:donp.org", 31 | "state_key": "@vbridge:donp.org", 32 | "type": "m.room.member", 33 | "unsigned": { 34 | "age": 378, 35 | "invi te_room_state": [ 36 | { 37 | "content": { 38 | "avatar_url": "mxc://donp.org/xWRPSLhFwLUjvaetqzSvPjnp", 39 | "displayname": "donpdonp", 40 | "membership": "join" 41 | }, 42 | "sender": "@donp:donp.org", 43 | "state_key": "@donp:donp.org", 44 | "type": "m.room.member" 45 | }, 46 | { 47 | " content": { 48 | "join_rule": "invite" 49 | }, 50 | "sender": "@donp:donp.org", 51 | "state_key": "", 52 | "type": "m.room.join_rules" 53 | } 54 | ] 55 | }, 56 | "user_id": "@donp:donp.org" 57 | } 58 | -------------------------------------------------------------------------------- /src/cli.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import setup 4 | import net 5 | import rpc 6 | import json 7 | import os 8 | 9 | fn mmain() { 10 | mut words := os.args[1..] // copy the system args 11 | verb := if words.len == 0 { 'status' } else { words[0] } 12 | config := setup.config() 13 | host := '127.0.0.1:$config.rpc_port' 14 | println(host) 15 | mut conn := connect(host) or { 16 | println('cant connect to $host $err') 17 | return 18 | } 19 | params := parse(words[1..]) 20 | cmd := build_cmd(verb, params) 21 | do(cmd, mut conn) 22 | println(conn.read_line()) 23 | } 24 | 25 | fn parse(words []string) map[string]string { 26 | mut params := map[string]string{} 27 | for word in words { 28 | parts := word.split(':') 29 | if parts.len > 1 { 30 | params[parts[0]] = parts[1] 31 | } 32 | } 33 | params['line'] = words.join(' ') 34 | return params 35 | } 36 | 37 | fn connect(host string) ?&net.TcpConn { 38 | conn := net.dial_tcp(host)? 39 | conn.peer_addr()? // dial_tcp always works so use this 40 | return conn 41 | } 42 | 43 | fn do(cmd rpc.Command, mut conn net.TcpConn) string { 44 | mut jstr := json.encode(cmd) 45 | jstr += '\n' 46 | println(jstr) 47 | conn.write(jstr.bytes()) or {} 48 | conn.wait_for_read() or {} 49 | line := conn.read_line() 50 | println('status data: $line') 51 | return line 52 | } 53 | 54 | fn irc_add(ihost string, mut conn net.TcpConn) { 55 | mut params := map[string]string{} 56 | params['host'] = ihost 57 | cmd := build_cmd('connect', params) 58 | jstr := json.encode(cmd) 59 | println(jstr) 60 | conn.write(jstr.bytes()) or {} 61 | } 62 | 63 | fn build_cmd(verb string, params map[string]string) rpc.Command { 64 | cmd := rpc.Command{ 65 | verb: verb 66 | params: params 67 | } 68 | return cmd 69 | } 70 | -------------------------------------------------------------------------------- /src/matrix/invite_room.json: -------------------------------------------------------------------------------- 1 | {"age":327,"content":{"displayname":"vbridge","membership":"invite"},"event_id":"$kCnvCzfP6BO-e_F5E9pefx-oTUJfvp2qyqA-_NShNng","invite_room_state":[{"content":{"avatar_url":"mxc:\/\/donp.org\/xWRPSLhFwLUjvaetqzSvPjnp","displayname":"donpdonp","membership":"join"},"sender":"@donp:donp.org","state_key":"@donp:donp.org","type":"m.room.member"},{"content":{"name":"Roomy Room"},"sender":"@donp:donp.org","state_key":"","type":"m.room.name"},{"content":{"alias":"#roomy-room:donp.org"},"sender":"@donp:donp.org","state_key":"","type":"m.room.canonical_alias"},{"content":{"url":"mxc:\/\/donp.org\/BkANcfXDSZklItkKGneDlqvF"},"sender":"@donp:donp.org","state_key":"","type":"m.room.avatar"},{"content":{"join_rule":"public"},"sender":"@donp:donp.org","state_key":"","type":"m.room.join_rules"}],"origin_server_ts":1615489245736,"prev_content":{"membership":"leave"},"replaces_state":"$Sdft63-MLYUjIBKjEAjeEDabj4Osn7WDHr4uA1RLMtY","room_id":"!FMIqCsoGJDbtjiBptb:donp.org","sender":"@donp:donp.org","state_key":"@vbridge:donp.org","type":"m.room.member","unsigned":{"age":327,"invite_room_state":[{"content":{"avatar_url":"mxc:\/\/donp.org\/xWRPSLhFwLUjvaetqzSvPjnp","displayname":"donpdonp","membership":"join"},"sender":"@donp:donp.org","state_key":"@donp:donp.org","type":"m.room.member"},{"content":{"name":"Roomy Room"},"sender":"@donp:donp.org","state_key":"","type":"m.room.name"},{"content":{"alias":"#roomy-room:donp.org"},"sender":"@donp:donp.org","state_key":"","type":"m.room.canonical_alias"},{"content":{"url":"mxc:\/\/donp.org\/BkANcfXDSZklItkKGneDlqvF"},"sender":"@donp:donp.org","state_key":"","type":"m.room.avatar"},{"content":{"join_rule":"public"},"sender":"@donp:donp.org","state_key":"","type":"m.room.join_rules"}],"prev_content":{"membership":"leave"},"prev_sender":"@vbridge:donp.org","replaces_state":"$Sdft63-MLYUjIBKjEAjeEDabj4Osn7WDHr4uA1RLMtY"},"user_id":"@donp:donp.org"} 2 | -------------------------------------------------------------------------------- /src/rpc/rpc.v: -------------------------------------------------------------------------------- 1 | module rpc 2 | 3 | import net 4 | import io 5 | import json 6 | import setup 7 | import matrix 8 | 9 | pub struct Actor { 10 | pub: 11 | out chan Command 12 | rpc_port string 13 | mut: 14 | clients []net.TcpConn 15 | } 16 | 17 | pub struct Command { 18 | pub: 19 | verb string 20 | params map[string]string 21 | mut: 22 | client_connection_id int 23 | } 24 | 25 | pub fn init(config setup.Config) &Actor { 26 | mut self := &Actor{ 27 | out: chan Command{} 28 | rpc_port: config.rpc_port 29 | } 30 | return self 31 | } 32 | 33 | pub fn (mut self Actor) listen() { 34 | mut l := net.listen_tcp(net.AddrFamily.ip, '$self.rpc_port') or { 35 | println('error opening rpc port $self.rpc_port') 36 | return 37 | } 38 | println('rpc listening $self.rpc_port') 39 | for { 40 | if conn := l.accept() { 41 | mut reader := io.new_buffered_reader(reader: conn) 42 | for { 43 | if line := reader.read_line() { 44 | mut cmd := json.decode(Command, line) or { panic(err) } 45 | cmd.client_connection_id = self.clients.len 46 | self.clients << conn 47 | self.out <- cmd 48 | // conn.close() 49 | } else { 50 | break 51 | } 52 | } 53 | } else { 54 | println('error accept() tcp') 55 | } 56 | } 57 | } 58 | 59 | struct StatusReport { 60 | host string 61 | conn_state matrix.ConnState 62 | } 63 | 64 | pub fn (mut self Actor) status(cmd Command, config setup.Config, matrix &matrix.Actor) { 65 | println('rpc_status client #$cmd.client_connection_id') 66 | mut conn := self.clients[cmd.client_connection_id] 67 | addr := conn.peer_addr() or { panic(err) } 68 | println(addr) 69 | status_report := StatusReport{ 70 | host: config.matrix_host 71 | conn_state: matrix.conn_state 72 | } 73 | mut jstr := json.encode(status_report) 74 | jstr += '\n' 75 | println('writing $jstr') 76 | conn.write_string(jstr) or { panic(err) } 77 | } 78 | -------------------------------------------------------------------------------- /src/appsvc/appsvc.v: -------------------------------------------------------------------------------- 1 | module appsvc 2 | 3 | import net 4 | import io 5 | import net.http 6 | import setup 7 | import x.json2 8 | import strings 9 | 10 | pub struct AppsvcActor { 11 | pub: 12 | out chan Command 13 | http_port string 14 | } 15 | 16 | pub struct Command { 17 | pub: 18 | data map[string]json2.Any 19 | } 20 | 21 | pub fn setup(config setup.Config) &AppsvcActor { 22 | return &AppsvcActor{ 23 | out: chan Command{} 24 | http_port: config.as_port 25 | } 26 | } 27 | 28 | pub fn (mut self AppsvcActor) listen() { 29 | mut l := net.listen_tcp(net.AddrFamily.ip, '$self.http_port') or { 30 | println('error opening appsvc port $self.http_port') 31 | return 32 | } 33 | for { 34 | mut conn := l.accept() or { panic('accept() failed: $err') } 35 | peer_ip := conn.peer_ip() or { err.msg() } 36 | mut reader := io.new_buffered_reader(reader: conn) 37 | response := http.parse_request(mut reader) or { 38 | println('appsvc http.parse response failed: $err') 39 | break 40 | } 41 | 42 | if response.header.contains_custom('Content-Length') { 43 | self.process_request(response.header, response.data) 44 | } 45 | 46 | respond(mut conn, 200, '{}') 47 | conn.close() or { println('appsvc socket close $peer_ip err $err') } 48 | } 49 | } 50 | 51 | pub fn (mut self AppsvcActor) process_request(headers http.Header, body string) { 52 | len_str := headers.get_custom('Content-Length') or { return } 53 | http_body_len := len_str.int() 54 | if body.len > 0 { 55 | if payload := json2.raw_decode(body) { 56 | events := payload.as_map()['events'].arr() 57 | for evt in events { 58 | self.out <- Command{ 59 | data: evt.as_map() 60 | } 61 | } 62 | } else { 63 | println('appsvc body decode error $err') 64 | } 65 | } else { 66 | println('appsvc http body empty. content-length $http_body_len but read $body.len bytes') 67 | } 68 | } 69 | 70 | fn read_headerlines(mut reader io.BufferedReader) []string { 71 | mut lines := []string{} 72 | for { 73 | mut line := reader.read_line() or { 74 | println('read_headerlines read_line() BAIL') 75 | return lines 76 | } 77 | l := line.trim('\r\n') 78 | if l == '' { 79 | break 80 | } 81 | lines << l 82 | } 83 | return lines 84 | } 85 | 86 | pub fn respond(mut conn net.TcpConn, status_code int, body string) { 87 | mut sb := strings.new_builder(1024) 88 | status := http.status_from_int(status_code) 89 | sb.write_string('HTTP/1.1 $status_code $status\r\n') 90 | sb.write_string('Content-Type: application/json\r\n') 91 | sb.write_string('Content-Length: $body.len\r\n') 92 | sb.write_string('Connection: close\r\n') 93 | sb.write_string('\r\n') 94 | headers := sb.str() 95 | conn.write(headers.bytes()) or {} 96 | conn.write(body.bytes()) or {} 97 | } 98 | -------------------------------------------------------------------------------- /src/db/db.v: -------------------------------------------------------------------------------- 1 | module db 2 | 3 | import sqlite 4 | import setup 5 | import log 6 | 7 | pub struct Db { 8 | sqlite sqlite.DB 9 | mut: 10 | log log.Log 11 | } 12 | 13 | type SqlType = int | string 14 | 15 | pub struct SqlValue { 16 | name string 17 | value SqlType 18 | } 19 | 20 | pub fn init(config setup.Config) Db { 21 | dbo := sqlite.connect('db.sqlite') or { 22 | println('cannot open db.sqlite $err') 23 | exit(1) 24 | } 25 | mut db := Db{ 26 | sqlite: dbo 27 | } 28 | db.log.set_full_logpath('log/sql') 29 | db.log.set_level(.debug) 30 | db.create_table('irc_servers', ['netname text', 'hostname text primary key', 'nick text']) 31 | db.create_table('irc_channels', ['channel text primary key', 'netname text']) 32 | db.create_table('matrix_rooms', ['room_id text primary key', 'name text']) 33 | db.create_table('bridge_rooms', ['left text primary key', 'right text']) 34 | db.create_table('bridge_nicks', ['matrix text primary key', 'irc text']) 35 | return db 36 | } 37 | 38 | pub fn (mut self Db) create_table(table string, fields []string) { 39 | self.exec_oneshot('create table if not exists $table (${fields.join(',')})') 40 | } 41 | 42 | pub fn (mut self Db) exec_oneshot(stmt string) { 43 | self.log.debug('sqlite: $stmt') 44 | self.sqlite.exec_one(stmt) or {} 45 | } 46 | 47 | pub fn (mut self Db) exec(stmt string) [][]string { 48 | rows, code := self.sqlite.exec(stmt) 49 | self.log.debug('sqlite: $stmt = $rows.len rows $code') 50 | println('sqlite: $stmt; [$rows.len rows. status $code]') 51 | strs := rows.map(it.vals) 52 | return strs 53 | } 54 | 55 | pub fn (mut self Db) insert(table string, parts []SqlValue) { 56 | mut fields := []string{} 57 | mut values := []string{} 58 | for part in parts { 59 | fields << part.name 60 | match part.value { 61 | int { 62 | values << part.value.str() 63 | } 64 | string { 65 | values << '"$part.value"' 66 | } 67 | } 68 | } 69 | stmt := 'insert into $table (${fields.join(',')}) values (${values.join(',')})' 70 | rows, code := self.sqlite.exec(stmt) 71 | self.log.debug('SQL $stmt $rows $code') 72 | println('sqlite: $stmt; [$rows.len rows. status $code]') 73 | } 74 | 75 | pub fn (mut self Db) update_by_field(table string, id_field string, id_value string, field string, value string) [][]string { 76 | zql := 'update $table set $field = "$value" where $id_field = "$id_value"' 77 | rows := self.exec(zql) 78 | self.log.debug('SQL $zql $rows') 79 | return rows 80 | } 81 | 82 | pub fn (mut self Db) delete_by_field(table string, field string, value string) { 83 | self.action_by_field('delete', table, field, value) 84 | } 85 | 86 | pub fn (mut self Db) select_by_field(table string, field string, value string) [][]string { 87 | return self.action_by_field('select *', table, field, value) 88 | } 89 | 90 | pub fn (mut self Db) action_by_field(action string, table string, field string, value string) [][]string { 91 | zql := '$action from $table where $field = "$value"' 92 | rows := self.exec(zql) 93 | self.log.debug('SQL $zql $rows') 94 | return rows 95 | } 96 | 97 | pub fn (mut self Db) select_all(table string) [][]string { 98 | stmt := 'select * from $table' 99 | return self.exec(stmt) 100 | } 101 | -------------------------------------------------------------------------------- /src/chat/chat.v: -------------------------------------------------------------------------------- 1 | module chat 2 | 3 | import setup 4 | import rand 5 | import msg 6 | import db 7 | 8 | type Payload = MakeIrcUser | MakeMatrixUser 9 | 10 | pub struct MakeIrcUser { 11 | pub: 12 | network_hostname string 13 | nick string 14 | } 15 | 16 | pub struct MakeMatrixUser {} 17 | 18 | pub struct Aliases { 19 | mut: 20 | aliases []&Alias 21 | } 22 | 23 | [heap] 24 | pub struct Alias { 25 | pub mut: 26 | matrix string 27 | irc string 28 | } 29 | 30 | pub struct Actor { 31 | pub mut: 32 | queue Queue 33 | out chan string 34 | cin chan Payload 35 | } 36 | 37 | pub struct Queue { 38 | pub mut: 39 | entries map[string]Say 40 | } 41 | 42 | pub enum System { 43 | irc 44 | matrix 45 | } 46 | 47 | type SayMsg = msg.IrcMsg | msg.MatrixMsg 48 | 49 | pub struct Say { 50 | pub: 51 | system System 52 | network string 53 | name string 54 | room string 55 | message string 56 | } 57 | 58 | pub fn (self Say) str() string { 59 | return '>$self.system< name: "$self.name" room: "$self.room" msg: "$self.message"' 60 | } 61 | 62 | pub fn setup(config setup.Config) &Actor { 63 | return &Actor{ 64 | out: chan string{cap: 100} 65 | cin: chan Payload{cap: 100} 66 | } 67 | } 68 | 69 | pub fn make_id() string { 70 | return rand.string(5) 71 | } 72 | 73 | pub fn (mut self Actor) say(chat System, name string, network string, room string, message string) { 74 | msg := Say{ 75 | system: chat 76 | network: network 77 | name: name 78 | room: room 79 | message: message 80 | } 81 | id := make_id() 82 | self.queue.set(id, msg) 83 | println('chat.say [$id/$self.queue.entries.len] $msg') 84 | match self.out.try_push(id) { 85 | .success {} 86 | .not_ready { println('WARNING chat.out channel not ready. channel len $self.out.len') } 87 | .closed {} 88 | } 89 | } 90 | 91 | pub fn (mut self Queue) set(id string, msg Say) { 92 | self.entries[id] = msg 93 | } 94 | 95 | pub fn (mut self Queue) get(id string) Say { 96 | return self.entries[id] 97 | } 98 | 99 | pub fn (mut self Queue) len() int { 100 | return self.entries.len 101 | } 102 | 103 | pub fn (mut self Queue) delete(id string) { 104 | self.entries.delete(id) 105 | } 106 | 107 | pub fn (mut self Queue) next() ?Say { 108 | id := self.next_id()? 109 | return self.entries[id] 110 | } 111 | 112 | pub fn (mut self Queue) next_id() ?string { 113 | if self.entries.len > 0 { 114 | return self.entries.keys()[0] 115 | } else { 116 | return error('empty') 117 | } 118 | } 119 | 120 | pub fn (mut self Queue) contains(name string) bool { 121 | return self.entries.keys().contains(name) 122 | } 123 | 124 | pub fn (mut self Queue) by_name(name string) []string { 125 | mut ids := []string{} 126 | for id, entry in self.entries { 127 | if entry.name == name { 128 | ids << id 129 | } 130 | } 131 | return ids 132 | } 133 | 134 | pub fn (mut self Aliases) match_irc(nick string) ?&Alias { 135 | for alias in self.aliases { 136 | if alias.irc == nick { 137 | println('chat.Aliases.match_irc $nick found $alias') 138 | return alias 139 | } 140 | } 141 | return error('not found') 142 | } 143 | 144 | pub fn (mut self Aliases) match_matrix(nick string) ?&Alias { 145 | for alias in self.aliases { 146 | if alias.matrix == nick { 147 | println('chat.Aliases.match_matrix $nick found $alias') 148 | return alias 149 | } 150 | } 151 | return error('not found') 152 | } 153 | 154 | pub fn (mut self Aliases) add(alias &Alias) &Alias { 155 | println('chat.Aliases.add $alias') 156 | self.aliases.prepend(alias) 157 | return self.aliases[0] 158 | } 159 | 160 | pub fn (mut self Alias) str() string { 161 | return 'matrix:$self.matrix irc:$self.irc' 162 | } 163 | 164 | pub fn alias_to_db(obj Alias) []db.SqlValue { 165 | mut row := []db.SqlValue{} 166 | row << db.SqlValue{ 167 | name: 'matrix' 168 | value: db.SqlType(obj.matrix) 169 | } 170 | row << db.SqlValue{ 171 | name: 'irc' 172 | value: db.SqlType(obj.irc) 173 | } 174 | return row 175 | } 176 | -------------------------------------------------------------------------------- /src/matrix/matrix.v: -------------------------------------------------------------------------------- 1 | module matrix 2 | 3 | import x.json2 4 | import net 5 | import net.http 6 | import net.urllib 7 | import setup 8 | import rand 9 | import regex 10 | import time 11 | import util 12 | 13 | enum ConnState { 14 | disconnected 15 | connected 16 | } 17 | 18 | type Payload = Connect | Disconnect | JoinRoom | MakeUser 19 | 20 | struct Connect {} 21 | 22 | struct Disconnect {} 23 | 24 | pub struct MakeUser { 25 | pub: 26 | user_id string 27 | name string 28 | } 29 | 30 | pub fn (self MakeUser) str() string { 31 | return '$self.user_id ($self.name)' 32 | } 33 | 34 | pub struct JoinRoom { 35 | pub: 36 | name string 37 | room string 38 | } 39 | 40 | pub struct Actor { 41 | pub: 42 | out chan Say 43 | cin chan Payload 44 | host string 45 | token string 46 | owner string 47 | pub mut: 48 | conn_state ConnState 49 | last_say time.Time 50 | joined_rooms Rooms 51 | whoami string 52 | } 53 | 54 | struct Rooms { 55 | pub mut: 56 | rooms []Room 57 | } 58 | 59 | [heap] 60 | pub struct Room { 61 | pub: 62 | id string 63 | name util.StringOrNone 64 | pub mut: 65 | user_ids []string 66 | } 67 | 68 | pub struct Say { 69 | pub: 70 | room Room 71 | message string 72 | } 73 | 74 | pub struct SayContext { 75 | pub: 76 | room_id string 77 | say chan Say 78 | } 79 | 80 | pub fn init(config setup.Config) &Actor { 81 | mut self := &Actor{ 82 | out: chan Say{} 83 | cin: chan Payload{cap: 100} 84 | host: config.matrix_host 85 | token: config.as_token 86 | owner: config.matrix_owner 87 | last_say: time.now() 88 | } 89 | return self 90 | } 91 | 92 | pub fn (mut self Actor) setup() { 93 | if matrix_id := self.whoami() { 94 | self.whoami = matrix_id 95 | makeuser := MakeUser{ 96 | user_id: matrix_id 97 | name: split(matrix_id)[1] 98 | } 99 | self.register(makeuser) or {} 100 | self.user_presence(matrix_id, 'online') or {} 101 | self.conn_state = ConnState.connected 102 | self.cin <- Payload(Connect{}) 103 | } else { 104 | println('matrix setup failed: $err') 105 | } 106 | } 107 | 108 | pub fn room_from_db(cols []string) &Room { 109 | return &Room{ 110 | id: cols[0] 111 | name: util.StringOrNone(cols[1]) 112 | } 113 | } 114 | 115 | pub fn rooms_subtract(rooms_a []&Room, rooms_b []&Room) []&Room { 116 | mut missing := []&Room{} 117 | for room in rooms_a { 118 | if rooms_contains(rooms_b, room) { 119 | } else { 120 | missing << room 121 | } 122 | } 123 | return missing 124 | } 125 | 126 | pub fn rooms_contains(rooms []&Room, looking Room) bool { 127 | for room in rooms { 128 | if room.id == looking.id { 129 | return true 130 | } 131 | } 132 | return false 133 | } 134 | 135 | pub fn (mut self Actor) listen() { 136 | } 137 | 138 | pub fn (mut self Actor) mxc_to_url(mxc string) string { 139 | servername, mediaid := mxc_split(mxc) 140 | url := 'https://$self.host/_matrix/media/r0/download/$servername/$mediaid' 141 | return url 142 | } 143 | 144 | pub fn mxc_split(mxc string) (string, string) { 145 | // "mxc:\/\/donp.org\/DBKlXYNItaxXzLDEgJwNdKBF" 146 | mxc_regex := r'mxc://([^/]+)/([^/]+)' 147 | mut re := regex.regex_opt(mxc_regex) or { panic(err) } 148 | re.match_string(mxc) 149 | mut parts := []string{} 150 | for g_index := 0; g_index < re.group_count; g_index++ { 151 | start, end := re.get_group_bounds_by_id(g_index) 152 | if start >= 0 { 153 | parts << mxc[start..end] 154 | } 155 | } 156 | return parts[0], parts[1] 157 | } 158 | 159 | pub fn (mut self Actor) try_pause() { 160 | duration := time.now() - self.last_say 161 | if duration.milliseconds() < 100 { 162 | println('matrix.say 500ms pause (last call was ${duration.milliseconds()}ms ago)') 163 | time.sleep(500 * time.millisecond) 164 | } 165 | self.last_say = time.now() 166 | } 167 | 168 | pub fn (mut self Actor) call_get(api string) ?(map[string]json2.Any, int) { 169 | return self.call(http.Method.get, api, '') 170 | } 171 | 172 | pub fn (mut self Actor) call(method http.Method, api string, body string) ?(map[string]json2.Any, int) { 173 | header := http.new_header(key: .authorization, value: 'Bearer $self.token') 174 | url := 'https://$self.host/_matrix/client/r0/$api' 175 | resp := http.fetch(method: method, data: body, url: url, header: header) or { 176 | return error('$url $err') 177 | } 178 | println('$method $url ($body.len) $body => [$resp.status_code] $resp.body') 179 | any := json2.raw_decode(resp.body)? 180 | return any.as_map(), resp.status_code 181 | } 182 | 183 | pub fn (mut self Actor) whoami() ?string { 184 | kv, _ := self.call_get('account/whoami') or { return error(err.msg()) } 185 | if user_id := kv['user_id'] { 186 | return user_id as string 187 | } else { 188 | mut errstr := '' 189 | if kv.keys().contains('errcode') { 190 | errcode := kv['errcode'] as string 191 | errstr = match errcode { 192 | 'M_UNKNOWN_TOKEN' { 'as_token rejected by $self.host' } 193 | else { kv['error'] as string } 194 | } 195 | } 196 | return error(errstr) 197 | } 198 | } 199 | 200 | pub fn split(id string) []string { 201 | mut parts := []string{} 202 | parts << id.substr(0, 1) 203 | left := id.substr(1, id.len) 204 | parts << left.before(':') 205 | parts << left.after(':') 206 | return parts 207 | } 208 | 209 | pub fn join(parts []string) string { 210 | return '@' + parts[0] + ':' + parts[1] 211 | } 212 | 213 | pub fn (mut self Actor) register(user MakeUser) ?string { 214 | mut user_data := map[string]json2.Any{} 215 | username := split(user.user_id)[1] 216 | user_data['username'] = username 217 | user_data['type'] = 'm.login.application_service' 218 | kv, _ := self.call(http.Method.post, 'register', user_data.str())? 219 | if 'errcode' in kv { 220 | return error('matrix.register() error! $kv') 221 | } else { 222 | if user_id_any := kv['user_id'] { 223 | user_id := user_id_any as string 224 | self.user_displayname(user_id, user.name) or { println('ERR: $err') } 225 | return user_id 226 | } else { 227 | return error('missing user_id') 228 | } 229 | } 230 | } 231 | 232 | pub fn (mut self Actor) sync() string { 233 | self.call_get('sync') or {} 234 | return '' 235 | } 236 | 237 | pub fn (mut self Actor) sync_user(user_id string) string { 238 | self.call_get('sync?user_id=$user_id') or {} 239 | return '' 240 | } 241 | 242 | pub fn (mut self Actor) joined_rooms() ?[]string { 243 | resp, _ := self.call_get('joined_rooms')? 244 | return resp['joined_rooms'].arr().map(it.str()) 245 | } 246 | 247 | pub fn (mut self Actor) join(room_id string) ?(map[string]json2.Any, int) { 248 | return self.join_as('', room_id) 249 | } 250 | 251 | pub fn (mut self Actor) join_as(user_id string, room_id string) ?(map[string]json2.Any, int) { 252 | user_part := if user_id.len > 0 { '?user_id=$user_id' } else { '' } 253 | return self.call(http.Method.post, 'rooms/$room_id/join$user_part', '') 254 | } 255 | 256 | pub fn (mut self Actor) leave(room_id string) { 257 | params, code := self.room_leave(room_id) or { return } 258 | println('matrix.leave $code $params') 259 | match code { 260 | 200 { 261 | self.joined_rooms.delete(room_id) 262 | } 263 | 404 { 264 | if params['errcode'].str() == 'M_UNKNOWN' { 265 | self.joined_rooms.delete(room_id) 266 | } 267 | } 268 | else {} 269 | } 270 | } 271 | 272 | pub fn (mut self Actor) room_leave(room_id string) ?(map[string]json2.Any, int) { 273 | return self.call(http.Method.post, 'rooms/$room_id/leave', '') 274 | } 275 | 276 | // not implemented in synapse until 1.36 (api r0.6.1) 277 | pub fn (mut self Actor) room_aliases(room_id string) ?[]string { 278 | params, _ := self.call_get('rooms/$room_id/aliases')? 279 | return params['aliases'].arr().map(it.str()) 280 | } 281 | 282 | pub fn (mut self Actor) room_joined_members(room_id string) ?[]string { 283 | mut members := []string{} 284 | params, code := self.call_get('rooms/$room_id/joined_members') or { return err } 285 | if code == 200 { 286 | for k, _ in params['joined'].as_map() { 287 | members << k 288 | } 289 | } 290 | return members 291 | } 292 | 293 | pub fn (mut self Actor) room_create(room_alias string) ?Room { 294 | mut user_data := map[string]json2.Any{} 295 | user_data['room_alias_name'] = room_alias 296 | user_data['is_direct'] = true 297 | params, code := self.call(http.Method.post, 'createRoom', user_data.str()) or { return err } 298 | if code == 200 { 299 | room := Room{ 300 | id: params['room_id'].str() 301 | name: room_alias 302 | } 303 | return room 304 | } else { 305 | return error('code $code') 306 | } 307 | } 308 | 309 | pub fn (mut self Actor) room_state(room_id string) ?string { 310 | self.call_get('rooms/$room_id/state')? 311 | return '' 312 | } 313 | 314 | pub fn (mut self Actor) room_messages(room Room) string { 315 | self.call_get('rooms/$room.id/messages') or {} 316 | return '' 317 | } 318 | 319 | pub fn (mut self Actor) room_say(room Room, msg string) string { 320 | self.room_say_as('', room, msg) 321 | return '' 322 | } 323 | 324 | pub fn (mut self Actor) room_invite(room Room, user string) ?bool { 325 | mut user_data := map[string]json2.Any{} 326 | user_data['user_id'] = user 327 | self.call(http.Method.post, 'rooms/$room.id/invite', user_data.str()) or { return err } 328 | return true 329 | } 330 | 331 | pub enum RoomSayAsReturn { 332 | good 333 | user_not_found 334 | not_in_room 335 | error 336 | } 337 | 338 | pub fn (mut self Actor) room_say_as(user_id string, room Room, msg string) RoomSayAsReturn { 339 | id := rand.string(6) 340 | mut evt := map[string]json2.Any{} 341 | if ctcp_msg := util.ctcp_decode(msg) { 342 | evt['msgtype'] = 'm.emote' 343 | evt['body'] = ctcp_msg.split(' ')[1..].join(' ') 344 | } else { 345 | evt['msgtype'] = 'm.text' 346 | evt['body'] = msg 347 | } 348 | user_part := if user_id.len > 0 { '?user_id=$user_id' } else { '' } 349 | resp, code := self.call(http.Method.put, 'rooms/$room.id/send/m.room.message/$id$user_part', 350 | evt.str()) or { 351 | println(err) 352 | return RoomSayAsReturn.error 353 | } 354 | if code == 403 { 355 | error := resp['error'].str() 356 | //{"errcode":"M_FORBIDDEN","error":"Application service has not registered this user"} 357 | if error.contains('has not registered') { 358 | return RoomSayAsReturn.user_not_found 359 | } else if error.contains('not in room') { 360 | //{"errcode":"M_FORBIDDEN","error":"User @ircbr_dpdp144:donp.org not in room !FMIqCsoGJDbtjiBptb:donp.org (None)"} 361 | return RoomSayAsReturn.not_in_room 362 | } 363 | } 364 | return RoomSayAsReturn.good 365 | } 366 | 367 | pub fn (mut self Actor) user_presence(user_id string, status string) ?string { 368 | mut evt := map[string]json2.Any{} 369 | evt['presence'] = status 370 | self.call(http.Method.put, 'presence/$user_id/status', evt.str())? 371 | return '' 372 | } 373 | 374 | pub fn (mut self Actor) user_display_name(user_id string) ?string { 375 | escaped_user_id := urllib.path_escape(user_id) 376 | url := 'profile/$escaped_user_id/displayname?user_id=$escaped_user_id' 377 | params, _ := self.call_get(url)? 378 | displayname := params['displayname'].str() 379 | return displayname 380 | } 381 | 382 | pub fn (mut self Actor) user_displayname(user_id string, displayname string) ?bool { 383 | mut name_data := map[string]json2.Any{} 384 | escaped_user_id := urllib.path_escape(user_id) 385 | url := 'profile/$escaped_user_id/displayname?user_id=$escaped_user_id' 386 | name_data['displayname'] = displayname 387 | _, status := self.call(http.Method.put, url, name_data.str())? 388 | return status == 200 389 | } 390 | 391 | struct RoomAliasErrNotFound { 392 | Error 393 | } 394 | 395 | pub fn (mut self Actor) room_alias(room_alias string) ?&Room { 396 | if room_alias.starts_with('#') { 397 | ret, code := self.call_get('directory/room/' + urllib.path_escape(room_alias))? 398 | if code == 404 { 399 | return IError(&RoomAliasErrNotFound{}) 400 | } else { 401 | return &Room{ 402 | name: room_alias 403 | id: ret['room_id'].str() 404 | } 405 | } 406 | } else { 407 | return error('$room_alias is not a room_alias') 408 | } 409 | } 410 | 411 | pub fn (self Actor) is_room(room string) bool { 412 | //#veloren:matrix.org 413 | room_match := r'^[!#].*:.*$' 414 | mut re := regex.regex_opt(room_match) or { panic('regex fail') } 415 | start, _ := re.match_string(room) 416 | return start > -1 417 | } 418 | 419 | pub fn (self Actor) invited_by(data map[string]json2.Any) string { 420 | mut alias := '' 421 | for s in data['invite_room_state'].arr() { 422 | state := s.as_map() 423 | mtype := state['type'].str() 424 | println('invite_by checking type $mtype in $state') 425 | if mtype == 'm.room.canonical_alias' { 426 | alias = state['content'].as_map()['alias'].str() 427 | println('invited by room $alias') 428 | } 429 | if mtype == 'm.room.member' { 430 | if alias.len == 0 { 431 | alias = state['sender'].str() 432 | println('invited by user $alias') 433 | } 434 | } 435 | } 436 | if alias.len == 0 { 437 | println('invite_by: warning no inviter found') 438 | } 439 | return alias 440 | } 441 | 442 | pub fn (mut self Rooms) replace(rooms []&Room) { 443 | self.rooms.clear() 444 | // VBUG self.rooms << rooms 445 | for r in rooms { 446 | self.rooms << r 447 | } 448 | } 449 | 450 | pub fn (mut self Rooms) add(room Room) { 451 | self.rooms << room 452 | } 453 | 454 | pub fn (mut self Rooms) delete(room_id string) { 455 | if idx := self.find_idx_by_id(room_id) { 456 | self.rooms.delete(idx) 457 | println('rooms.delete() $room_id') 458 | } else { 459 | println('rooms.delete() $room_id $err') 460 | } 461 | } 462 | 463 | pub fn (mut self Rooms) find_idx_by_id(room_id string) ?int { 464 | for idx, room in self.rooms { 465 | if room.id == room_id { 466 | return idx 467 | } 468 | } 469 | return error('not found') 470 | } 471 | 472 | pub fn (mut self Rooms) len() int { 473 | return self.rooms.len 474 | } 475 | 476 | pub fn (mut self Rooms) dm(user string) ?Room { 477 | return self.find_room_by_name(user) 478 | } 479 | 480 | pub fn (mut self Rooms) find_room_by_name(name string) ?Room { 481 | for room in self.rooms { 482 | if room.name is string { // VBUG room_name := room.name { 483 | if room.name == name { 484 | return room 485 | } 486 | } 487 | } 488 | return error("room name \"$name\" not found") 489 | } 490 | 491 | pub fn (mut self Rooms) find_room_by_id(room_id string) ?Room { 492 | for room in self.rooms { 493 | if room.id == room_id { 494 | return room 495 | } 496 | } 497 | return error("room \"$room_id\" not found") 498 | } 499 | 500 | pub fn (mut self Rooms) room_by_partial_name(room_name string) ?&Room { 501 | println('room_name_by_partial_name: searching $room_name in $self.rooms.len rooms') 502 | for mut room in self.rooms { 503 | if room.name is string { // this_room_name := room.name { 504 | partial_name := room.name.before(':') 505 | println('room_name comparing $partial_name to $room_name') 506 | if partial_name == room_name { 507 | return room 508 | } 509 | } 510 | } 511 | return error('room_partial_name not in room list') 512 | } 513 | 514 | pub fn (mut self Rooms) by_id(room_id string) ?&Room { 515 | for mut room in self.rooms { 516 | if room.id == room_id { 517 | return room 518 | } 519 | } 520 | return error('$room_id not in room list') 521 | } 522 | 523 | pub fn (mut self Rooms) pointers() []&Room { 524 | mut ptrs := []&Room{} 525 | for mut room in self.rooms { 526 | ptrs << room 527 | } 528 | return ptrs 529 | } 530 | 531 | pub fn (self Rooms) str() string { 532 | return self.rooms.str() 533 | } 534 | 535 | pub fn (self Room) str() string { 536 | name := if self.name is string { self.name } else { 'None' } 537 | return '"$name"($self.id)' 538 | } 539 | -------------------------------------------------------------------------------- /src/irc/irc.v: -------------------------------------------------------------------------------- 1 | module irc 2 | 3 | import net 4 | import io 5 | import regex 6 | import time 7 | import db 8 | import chat 9 | import util 10 | import strings 11 | 12 | const ( 13 | irc_msg_regex = r'^([^ ]+) ([^ ]+)( :?([^ ]+)( :?(.*))?)?' 14 | irc_extra_regex = r'(\S*)(\s+:?([^:]+))?$' 15 | ) 16 | 17 | pub struct IrcActor { 18 | pub: 19 | out chan chat.Say 20 | cin chan Payload 21 | pub mut: 22 | networks Networks 23 | puppets Puppets 24 | } 25 | 26 | type Payload = Connect | Disconnect | Joined | NickInUse | PrivMsg 27 | 28 | struct NickInUse { 29 | pub mut: 30 | puppet &Puppet 31 | } 32 | 33 | pub struct Connect { 34 | pub: 35 | puppet &Puppet 36 | } 37 | 38 | struct Disconnect { 39 | pub mut: 40 | puppet &Puppet 41 | } 42 | 43 | struct Joined { 44 | pub: 45 | puppet &Puppet 46 | channel string 47 | } 48 | 49 | struct PrivMsg { 50 | pub: 51 | channel string 52 | puppet Puppet 53 | nick string 54 | pub mut: 55 | message string 56 | } 57 | 58 | pub enum ConnState { 59 | connected 60 | disconnected 61 | } 62 | 63 | [heap] 64 | pub struct Network { 65 | pub mut: 66 | name string 67 | hostname string 68 | nick string 69 | } 70 | 71 | pub struct Channels { 72 | pub mut: 73 | channels []&Channel 74 | } 75 | 76 | [heap] 77 | pub struct Channel { 78 | pub mut: 79 | name string 80 | joined bool 81 | } 82 | 83 | pub struct Networks { 84 | pub mut: 85 | networks []&Network 86 | } 87 | 88 | pub struct Puppets { 89 | pub mut: 90 | puppets []&Puppet 91 | } 92 | 93 | type SockOrNone = net.TcpConn | util.None 94 | 95 | [heap] 96 | pub struct Puppet { 97 | pub mut: 98 | nick string 99 | state ConnState = ConnState.disconnected 100 | sock SockOrNone 101 | channels Channels 102 | stop bool 103 | network &Network 104 | } 105 | 106 | pub fn setup() &IrcActor { 107 | actor := &IrcActor{ 108 | cin: chan Payload{cap: 100} 109 | } 110 | return actor 111 | } 112 | 113 | pub enum SayReturn { 114 | good 115 | network_not_found 116 | user_not_found 117 | error 118 | } 119 | 120 | pub fn (mut self IrcActor) say(network string, nick string, room string, message string) SayReturn { 121 | mut ircnet := self.networks.by_name(network) or { 122 | println('irc.say() network "$network" not found. (msg was: $message)') 123 | return .network_not_found 124 | } 125 | mut puppet := self.puppets.by_net_nick(ircnet, nick) or { 126 | println('irc.say() puppet nick "$nick" not found in ${network}. (msg was: $message)') 127 | return .user_not_found 128 | } 129 | cmd := 'PRIVMSG $room :$message' 130 | puppet.write(cmd) or { return .error } 131 | return .good 132 | } 133 | 134 | pub fn network_from_db(cols []string) &Network { 135 | return &Network{ 136 | name: cols[0] 137 | hostname: cols[1] 138 | nick: cols[2] 139 | } 140 | } 141 | 142 | pub fn network_to_db(ircnet Network) []db.SqlValue { 143 | mut ircrow := []db.SqlValue{} 144 | ircrow << db.SqlValue{ 145 | name: 'hostname' 146 | value: db.SqlType(ircnet.hostname) 147 | } 148 | ircrow << db.SqlValue{ 149 | name: 'netname' 150 | value: db.SqlType(ircnet.name) 151 | } 152 | ircrow << db.SqlValue{ 153 | name: 'nick' 154 | value: db.SqlType(ircnet.nick) 155 | } 156 | return ircrow 157 | } 158 | 159 | pub fn (self Network) str() string { 160 | mut summary := '' 161 | summary = '$self.name/$self.hostname' 162 | return summary 163 | } 164 | 165 | pub fn (mut self Networks) by_hostname_idx(hostname string) int { 166 | for idx, ircnet in self.networks { 167 | // vbug if net == ircnet 168 | if hostname == ircnet.hostname { 169 | return idx 170 | } 171 | } 172 | println('error! by_hostname_idx not found $hostname in $self') 173 | return -1 174 | } 175 | 176 | pub fn (mut self Networks) by_hostname(addr string) ?&Network { 177 | for mut ircnet in self.networks { 178 | mut p_ircnet := *ircnet // vbug 179 | if p_ircnet.hostname == addr { 180 | return p_ircnet 181 | } 182 | } 183 | println('irc.Networks.by_hostname $addr not found in $self.networks') 184 | return error('server not found') 185 | } 186 | 187 | pub fn (mut self Puppet) add_channel(name string) { 188 | if _ := self.find_channel(name) { 189 | } else { 190 | self.channels.channels << &Channel{ 191 | name: name 192 | joined: true 193 | } 194 | } 195 | } 196 | 197 | pub fn (self Puppet) find_channel(channel_name string) ?&Channel { 198 | for channel in self.channels.channels { 199 | if channel.name == channel_name { 200 | return channel 201 | } 202 | } 203 | return error('not found') 204 | } 205 | 206 | pub fn (mut self IrcActor) connect_all() { 207 | for mut ircnet in self.networks.networks { 208 | mut n_ircnet := *ircnet // vbug 209 | ghost := self.puppets.by_net_nick(n_ircnet, n_ircnet.nick) or { 210 | mut n_ghost := self.new_ghost(mut n_ircnet, n_ircnet.nick) 211 | self.connect(mut n_ghost) 212 | self.puppets.add(mut n_ghost) 213 | } 214 | mut g_ghost := *ghost // vbug 215 | println('irc.connect_all $n_ircnet $g_ghost.nick') 216 | if g_ghost.state == ConnState.disconnected { 217 | g_ghost.dial() 218 | } 219 | } 220 | } 221 | 222 | pub fn dial(hostname string, nick string) SockOrNone { 223 | mut host := hostname 224 | if !host.contains(':') { 225 | host = host + ':6667' 226 | } 227 | println('irc.dial() connecting $host $nick') 228 | mut sock := net.dial_tcp(host) or { return SockOrNone(util.None{}) } 229 | 230 | sock.set_read_timeout(5 * time.minute) 231 | return SockOrNone(sock) 232 | } 233 | 234 | pub fn (mut self Puppet) dial() { 235 | println('$self.nick dial($self.network.hostname, $self.nick)') 236 | if self.state == .connected { 237 | println('$self.nick dial: already connected.') 238 | } else { 239 | self.sock = dial(self.network.hostname, self.nick) 240 | if self.sock is net.TcpConn { 241 | println('$self.nick dial() connected') 242 | self.state = .connected 243 | } else { 244 | println('$self.nick dial() failed') 245 | } 246 | } 247 | } 248 | 249 | pub fn (mut self Puppet) join(channel string) { 250 | println('$self.nick joining $channel') 251 | cmd := 'JOIN $channel' 252 | self.write(cmd) or {} 253 | } 254 | 255 | pub fn (self IrcActor) find_ghost_idx(nick string) int { 256 | for idx, ghost in self.puppets.puppets { 257 | if ghost.nick == nick { 258 | return idx 259 | } 260 | } 261 | return -1 262 | } 263 | 264 | pub fn (mut self IrcActor) new_ghost(mut ircnet Network, nick string) &Puppet { 265 | mut puppet := &Puppet{ 266 | nick: nick 267 | network: ircnet 268 | } 269 | return puppet 270 | } 271 | 272 | pub fn (mut self IrcActor) connect(mut puppet Puppet) { 273 | puppet.dial() 274 | if puppet.sock is net.TcpConn { 275 | go self.comm(mut puppet) 276 | puppet.signin() 277 | } else { 278 | println('WARNING: sock connection failed for $puppet') 279 | } 280 | } 281 | 282 | pub fn (mut self Puppet) signin() { 283 | nick_cmd := 'nick $self.nick' 284 | self.write(nick_cmd) or {} 285 | user_cmd := 'user vbridge b c :full name' 286 | self.write(user_cmd) or {} 287 | } 288 | 289 | pub fn (mut self Puppets) hangup(ircnet &Network) { 290 | for mut puppet in self.puppets { 291 | mut p_pup := *puppet 292 | if p_pup.network.name == ircnet.name { 293 | p_pup.hangup() 294 | } 295 | } 296 | } 297 | 298 | pub fn (self &IrcActor) comm(mut puppet Puppet) { 299 | if mut puppet.sock is net.TcpConn { 300 | println('$puppet.nick comm() started') 301 | mut reader := io.new_buffered_reader(reader: puppet.sock) 302 | for { 303 | if line := reader.read_line() { 304 | _ := self.proto(line, mut puppet) 305 | } else { 306 | println('$puppet.network $puppet.nick comm() TCP closed $err') 307 | puppet.hangup() 308 | payload := Payload(Disconnect{ 309 | puppet: puppet 310 | }) 311 | match self.cin.try_push(payload) { 312 | .success {} 313 | .not_ready { println('WARNING irc.cin channel not ready. channel len $self.cin.len') } 314 | .closed {} 315 | } 316 | 317 | // self.cin <- payload 318 | break 319 | } 320 | if puppet.stop { 321 | break 322 | } 323 | } 324 | println('$puppet.nick comm() stopped') 325 | } else { 326 | println('WARNING: $puppet.nick comm() called with missing socket') 327 | } 328 | } 329 | 330 | pub fn (self &IrcActor) proto(line string, mut puppet Puppet) string { 331 | // println(line) 332 | parts := parse(line) 333 | if parts.len > 0 { 334 | word := if parts.len == 2 { 335 | parts[0] 336 | } else { 337 | if parts.len > 2 { 338 | parts[1] 339 | } else { 340 | println('irc.proto parse err $parts') 341 | 'E_PARSEERR' 342 | } 343 | } 344 | match word { 345 | '001' { 346 | puppet.nick = parts[3] // nick confirmed 347 | } 348 | '002' {} 349 | '003' {} 350 | '004' {} 351 | '005' { 352 | capabilities := capabilities_decode(parts[2]) 353 | if netname := capabilities['NETWORK'] { 354 | println('$puppet.network.hostname is part of network $netname') 355 | puppet.network.name = netname 356 | } 357 | } 358 | '250' { 359 | // highest connection count 360 | } 361 | '251' {} 362 | '252' {} 363 | '253' {} 364 | '254' {} 365 | '255' {} 366 | '265' {} 367 | '266' {} 368 | '332' { 369 | // room topic 370 | } 371 | '333' { 372 | // room topic set at 373 | } 374 | '353' { 375 | // room nick list 376 | } 377 | '366' { 378 | // end of nicks 379 | } 380 | '372' { 381 | // drop motd 382 | } 383 | '376' { 384 | // end of motd 385 | println('irc.proto 376 end of motd - Connect $puppet.network.name $puppet.nick') 386 | puppet.state = .connected 387 | self.cin <- Payload(Connect{ 388 | puppet: puppet 389 | }) 390 | } 391 | '433' { 392 | println('$puppet irc.proto 433 nick $puppet.nick already in use! aborting connection') 393 | self.cin <- Payload(NickInUse{ 394 | puppet: puppet 395 | }) 396 | } 397 | 'NICK' { 398 | //[':donpdonp|z_!~donp@1.2.3.4', 'NICK', ' :donpdonp|z', 'donpdonp|z'] 399 | println(parts) // debug 400 | nick_parts := nick_parse(parts[0][1..]) 401 | if nick_parts[0] == puppet.nick { 402 | println('NICK changing puppet $puppet.nick to ${parts[3]}') 403 | puppet.nick = parts[3] 404 | } else { 405 | println('$puppet.nick heard NICK change for ${nick_parts[0]} -> ${parts[3]}. ignoring') 406 | } 407 | } 408 | 'NOTICE' {} 409 | 'JOIN' { 410 | println('sock ${ptr_str(puppet.sock)} $line') 411 | //:donp|m!~a@64.62.134.149 JOIN #roomy-room 412 | nick_parts := nick_parse(parts[0][1..]) 413 | channel := parts[3] 414 | println('$puppet.network $puppet.nick puppet ${ptr_str(puppet)}: ${nick_parts[0]} JOINed $channel') 415 | if nick_parts[0] == puppet.nick { 416 | puppet.add_channel(channel) 417 | self.cin <- Payload(Joined{ 418 | puppet: puppet 419 | channel: channel 420 | }) 421 | } 422 | } 423 | 'PING' { 424 | reply := 'PONG ${parts[1]}' 425 | puppet.write(reply) or {} 426 | } 427 | 'PRIVMSG' { 428 | // [':donpdonp!~vbridge@64.62.134.149', 'PRIVMSG', ' #robots :hi there', '#robots', ' :hi there', 'hi there'] 429 | mut privmsg := PrivMsg{ 430 | channel: parts[3] 431 | puppet: puppet 432 | nick: parts[0][1..] // remove : from protocol 433 | message: '' 434 | } 435 | if ctcp := util.ctcp_decode(parts[5]) { 436 | ctcp_parts := ctcp.split(' ') 437 | match ctcp_parts[0] { 438 | 'VERSION' {} 439 | 'ACTION' { 440 | privmsg.message = parts[5] 441 | self.cin <- Payload(privmsg) 442 | } 443 | else { 444 | privmsg.message = 'unknown CTCP: ' + ctcp 445 | self.cin <- Payload(privmsg) 446 | } 447 | } 448 | } else { 449 | privmsg.message = color_strip(parts[5]) 450 | self.cin <- Payload(privmsg) 451 | } 452 | } 453 | 'MODE' { 454 | //:tbridge MODE tbridge :+i 455 | } 456 | else { 457 | println('$word $parts') 458 | } 459 | } 460 | } 461 | return '' 462 | } 463 | 464 | pub fn color_strip(msg string) string { 465 | // https://modern.ircdocs.horse/formatting.html 466 | mut new_msg := strings.new_builder(msg.len) 467 | runes := msg.runes() 468 | msg_len := runes.len 469 | for idx := 0; idx < msg_len; idx += 1 { 470 | chr := runes[idx] 471 | if chr == 0x03 { // start color 472 | idx++ 473 | idx++ 474 | } else { 475 | new_msg.write_rune(chr) 476 | } 477 | } 478 | return new_msg.str() 479 | } 480 | 481 | pub fn nick_parse(full_nick string) []string { 482 | // donp|m!~a@64.62.134.149 483 | mut parts := []string{} 484 | parts << full_nick.before('!') 485 | return parts 486 | } 487 | 488 | pub fn parse(line string) []string { 489 | mut parts := []string{} 490 | mut re := regex.regex_opt(irc.irc_msg_regex) or { panic(err) } 491 | re.match_string(line.trim_string_right('\n')) 492 | for g_index := 0; g_index < re.group_count; g_index++ { 493 | start, end := re.get_group_bounds_by_id(g_index) 494 | if start >= 0 { 495 | parts << line[start..end] 496 | } 497 | } 498 | return parts 499 | } 500 | 501 | pub fn (self &IrcActor) is_room(room string) bool { 502 | room_match := r'^#[-A-Za-z0-9_]+$' 503 | mut re := regex.regex_opt(room_match) or { panic('regex fail') } 504 | start, _ := re.match_string(room) 505 | println('irc:is_room $room $room_match $start') 506 | return start > -1 507 | } 508 | 509 | pub fn (self PrivMsg) str() string { 510 | return '$self.puppet.network $self.puppet.nick: $self.channel $self.nick $self.message' 511 | } 512 | 513 | pub fn (self Channels) find_by_name(name string) ?&Channel { 514 | for channel in self.channels { 515 | println('irc.find_by_name [mtx] $name == [irc] $channel.name') 516 | if channel.name == name { 517 | return channel 518 | } 519 | } 520 | return error('not found') 521 | } 522 | 523 | pub fn (mut self IrcActor) find_channel_by_name(nick string, channel_name string) ?&Channel { 524 | if ghost := self.puppets.by_nick(nick) { 525 | if channel := ghost.channels.find_by_name(channel_name) { 526 | return channel 527 | } else { 528 | println('irc.find_channel_by_name() $ghost.nick ghost ${ptr_str(ghost)} has no $channel_name') 529 | dump(ghost.channels) 530 | } 531 | } 532 | return error('not found') 533 | } 534 | 535 | pub fn (self Puppets) by_network(netname string) []&Puppet { 536 | mut winners := []&Puppet{} 537 | for g in self.puppets { 538 | if g.network.name == netname { 539 | winners << g 540 | } 541 | } 542 | return winners 543 | } 544 | 545 | pub fn (mut self Puppets) by_nick(nick string) ?&Puppet { 546 | for mut puppet in self.puppets { 547 | p_pup := *puppet 548 | if p_pup.nick == nick { 549 | println("irc.Puppets.by_nick(\"$nick\") found ghost.nick ${ptr_str(p_pup.nick)} \"$p_pup.nick\" ") 550 | return p_pup 551 | } 552 | } 553 | return error('not found') 554 | } 555 | 556 | pub fn (mut self Networks) add(network &Network) &Network { 557 | self.networks << network 558 | return self.networks.last() // network was copied. return copy. 559 | } 560 | 561 | pub fn (mut self Networks) by_name(name string) ?&Network { 562 | for mut puppet in self.networks { 563 | mut p_net := *puppet 564 | if p_net.name == name { 565 | return p_net 566 | } 567 | } 568 | return error('not found') 569 | } 570 | 571 | pub fn (self Channel) str() string { 572 | return '$self.name' 573 | } 574 | 575 | pub fn (self Puppets) by_net_nick(ircnet Network, nick string) ?&Puppet { 576 | for g in self.puppets { 577 | if g.nick == nick && g.network.name == ircnet.name { 578 | println("irc.Puppets.by_net_nick(\"$ircnet.name\" \"$nick\") found ghost.nick ${ptr_str(g.nick)} \"$g.nick\" ") 579 | return g 580 | } 581 | } 582 | return error('not found') 583 | } 584 | 585 | pub fn (mut self Puppet) nick(nick string) { 586 | self.write('NICK $nick') or {} 587 | } 588 | 589 | pub fn (mut self Puppet) write(msg string) ? { 590 | if mut self.sock is net.TcpConn { 591 | println('$self.network $self.nick: $msg') 592 | self.sock.write_string(msg + '\n') or { 593 | println('$self.network $self.nick: socket write error $err') 594 | self.hangup() 595 | } 596 | } else { 597 | errmsg := 'NO SOCK: $self.network $self.nick: dropped $msg' 598 | println(errmsg) 599 | return error(errmsg) 600 | } 601 | } 602 | 603 | pub fn (mut self Puppet) hangup() { 604 | println('$self.nick hangup()') 605 | if mut self.sock is net.TcpConn { 606 | println('$self.nick sock.close()') 607 | self.sock.close() or { println('$self.nick sock.close $err') } 608 | } 609 | self.state = ConnState.disconnected 610 | self.channels.channels.clear() 611 | } 612 | 613 | pub fn (self Puppet) str() string { 614 | return '${ptr_str(self)} nick:$self.nick sock:${ptr_str(self.sock)} conn: $self.state channels:$self.channels' 615 | } 616 | 617 | pub fn (mut self Puppets) add(mut puppet Puppet) &Puppet { 618 | self.puppets << puppet 619 | return self.puppets.last() // puppet was copied. return copy. 620 | } 621 | 622 | pub fn capabilities_decode(capstr string) map[string]string { 623 | parts := capstr.split(' ') 624 | mut cap := map[string]string{} 625 | for part in parts { 626 | cap_parts := part.split('=') 627 | if cap_parts.len == 2 { 628 | key := cap_parts[0] 629 | value := cap_parts[1] 630 | cap[key] = value 631 | } 632 | } 633 | return cap 634 | } 635 | -------------------------------------------------------------------------------- /src/app.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import time 4 | import setup 5 | import irc 6 | import matrix 7 | import rpc 8 | import appsvc 9 | import db 10 | import chat 11 | import regex 12 | import util 13 | import bridge 14 | import x.json2 15 | 16 | struct Main { 17 | mut: 18 | irc &irc.IrcActor 19 | appsvc &appsvc.AppsvcActor 20 | matrix &matrix.Actor 21 | rpc &rpc.Actor 22 | config setup.Config 23 | db db.Db 24 | chat &chat.Actor 25 | aliases chat.Aliases 26 | } 27 | 28 | fn main() { 29 | config := setup.config() 30 | mut main := &Main{ 31 | irc: irc.setup() 32 | appsvc: appsvc.setup(config) 33 | matrix: matrix.init(config) 34 | rpc: rpc.init(config) 35 | config: config 36 | db: db.init(config) 37 | chat: chat.setup(config) 38 | } 39 | go main.listen_out() 40 | go main.matrix.setup() 41 | go main.appsvc.listen() 42 | go main.rpc.listen() 43 | go main.matrix.listen() 44 | go main.irc.listen() 45 | irchosts := main.db.select_all('irc_servers') 46 | for row in irchosts { 47 | ircnet := irc.network_from_db(row) 48 | main.irc.networks.add(ircnet) 49 | } 50 | println('loaded $irchosts.len irc networks') 51 | aliases := main.db.select_all('bridge_nicks') 52 | for alias in aliases { 53 | main.aliases.add(&chat.Alias{ matrix: alias[0], irc: alias[1] }) 54 | } 55 | println('loaded $aliases.len matrix/irc aliases') 56 | main.mainloop() 57 | } 58 | 59 | fn (mut self Main) mainloop() { 60 | for { 61 | select { 62 | irc_m := <-self.irc.cin { 63 | self.irc_do(irc_m) 64 | } 65 | matrix := <-self.matrix.cin { 66 | self.matrix_do(matrix) 67 | } 68 | appsvc := <-self.appsvc.out { 69 | self.as_do(appsvc) 70 | } 71 | rpc := <-self.rpc.out { 72 | self.rpc_do(rpc) 73 | } 74 | chat_payload := <-self.chat.cin { 75 | self.chat_do(chat_payload) 76 | } 77 | 60 * time.minute { 78 | println('$time.now()') 79 | } 80 | } 81 | } 82 | } 83 | 84 | fn (mut self Main) chat_do(payload chat.Payload) { 85 | println(payload) 86 | match payload { 87 | chat.MakeIrcUser { 88 | mut ircnet := self.irc.networks.by_name(payload.network_hostname) or { 89 | println('chat_do() not found: $payload.network_hostname') 90 | return 91 | } 92 | mut puppet := self.irc.new_ghost(mut ircnet, payload.nick) 93 | self.irc.connect(mut puppet) 94 | self.irc.puppets.add(mut puppet) 95 | } 96 | else {} 97 | } 98 | } 99 | 100 | fn (mut self Main) matrix_do(payload matrix.Payload) { 101 | match payload { 102 | matrix.Connect { 103 | joined_room_ids := self.matrix.joined_rooms() or { 104 | println('$err') 105 | return 106 | } 107 | mut matrix_rooms := []&matrix.Room{} 108 | for jroom_id in joined_room_ids { 109 | db_room := self.db.select_by_field('matrix_rooms', 'room_id', jroom_id) 110 | mut mroom := &matrix.Room{} 111 | if db_room.len > 0 { 112 | mroom = matrix.room_from_db(db_room[0]) 113 | } else { 114 | println('warning: joined room $jroom_id not found in db') 115 | mroom = &matrix.Room{ 116 | id: jroom_id 117 | name: util.StringOrNone(&util.None{}) 118 | } 119 | } 120 | println('matrix room loaded $mroom') 121 | matrix_rooms << mroom 122 | } 123 | self.matrix.joined_rooms.replace(matrix_rooms) 124 | println('Matrix $self.config.matrix_host sync_rooms') 125 | self.sync_matrix_rooms() 126 | println('Matrix $self.config.matrix_host SETUP done') 127 | alias := &chat.Alias{ 128 | matrix: self.matrix.whoami 129 | irc: matrix.split(self.matrix.whoami)[1] 130 | } 131 | self.aliases.add(alias) 132 | self.admin_say('* vbridge started. I am $self.matrix.whoami') 133 | // start IRC setup after matrix 134 | println('connecting $self.irc.networks.networks.len irc networks') 135 | go self.irc.connect_all() 136 | } 137 | matrix.MakeUser { 138 | // register user, join room, and try again 139 | if _ := self.matrix.register(payload) { 140 | self.admin_say('registered $payload') 141 | self.process_out_user(payload.user_id) 142 | } else { 143 | self.admin_say('matrix registration error for $payload: $err') 144 | } 145 | } 146 | matrix.JoinRoom { 147 | self.matrix.join_as(payload.name, payload.room) or {} 148 | self.process_out_user(payload.name) 149 | } 150 | else {} 151 | } 152 | } 153 | 154 | fn (mut self Main) irc_do(irc_m irc.Payload) { 155 | match irc_m { 156 | irc.PrivMsg { 157 | println('<- $irc_m') 158 | if irc_m.channel == irc_m.puppet.network.nick { 159 | room := irc_m.nick[1..].split('!')[0] 160 | self.command(chat.System.irc, irc_m.puppet.network.name, irc_m.nick, room, 161 | irc_m.message[1..]) 162 | } else { 163 | // when bridgebot hears it, repeat in matrix 164 | if irc_m.puppet.nick == irc_m.puppet.network.nick { 165 | partial_irc_nick := irc_m.nick.split('!')[0] 166 | if _ := self.irc.puppets.by_net_nick(irc_m.puppet.network, partial_irc_nick) { 167 | println('info: $partial_irc_nick is a ghost. not relaying.') 168 | } else { 169 | full_channel := '$irc_m.channel:$irc_m.puppet.network.name' 170 | if room_name := bridge.matching_room(full_channel, mut self.db) { 171 | if mroom := self.matrix.joined_rooms.find_room_by_name(room_name) { 172 | matrix_name := self.name_convert(chat.System.irc, partial_irc_nick) 173 | self.chat.say(chat.System.matrix, matrix_name, '', mroom.id, 174 | irc_m.message) 175 | } else { 176 | println('warning: no matrix.joined_rooms for $room_name') 177 | } 178 | } else { 179 | println('warning: no bridge found for $full_channel msg dropped: $irc_m.message') 180 | } 181 | } 182 | } 183 | } 184 | } 185 | irc.Connect { 186 | user_id := self.name_convert(.irc, irc_m.puppet.nick) 187 | msg := '${irc_m.puppet.nick}($user_id) connected to $irc_m.puppet.network' 188 | self.matrix_say(user_id, msg) 189 | self.sync_irc_channels(irc_m.puppet) 190 | } 191 | irc.Disconnect { 192 | user_id := self.name_convert(.irc, irc_m.puppet.nick) 193 | msg := '$irc_m.puppet.nick disconnected from $irc_m.puppet.network.hostname use !irc connect' 194 | self.matrix_say(user_id, msg) 195 | mut puppet := self.irc.puppets.by_nick(irc_m.puppet.nick) or { return } // mut hack 196 | self.irc.connect(mut puppet) 197 | } 198 | irc.Joined { 199 | user_id := self.name_convert(.irc, irc_m.puppet.nick) 200 | msg := '$irc_m.puppet.nick joined $irc_m.channel on $irc_m.puppet.network.name' 201 | self.matrix_say(user_id, msg) 202 | self.process_out_user(irc_m.puppet.nick) 203 | } 204 | irc.NickInUse { 205 | user_id := self.name_convert(.irc, irc_m.puppet.nick) 206 | msg := '$irc_m.puppet.nick is already in use on $irc_m.puppet.network.name' 207 | self.matrix_say(user_id, msg) 208 | mut puppet := self.irc.puppets.by_nick(irc_m.puppet.nick) or { return } // mut hack 209 | puppet.hangup() 210 | } 211 | } 212 | } 213 | 214 | fn (mut self Main) rpc_do(cmd rpc.Command) { 215 | self.command(chat.System.irc, 'rpc', 'rpc', 'rpc', '$cmd.verb ${cmd.params['line']}') 216 | } 217 | 218 | fn (mut self Main) as_do(cmd appsvc.Command) { 219 | // println('as_do: ${cmd.data['id']} ${cmd.data['type']}') 220 | match cmd.data['type'].str() { 221 | 'm.room.name' { 222 | println('m.room.name $cmd') 223 | } 224 | 'm.room.member' { 225 | println('m.room.member $cmd') 226 | c := cmd.data['content'].as_map() 227 | room_id := cmd.data['room_id'].str() 228 | whoami := cmd.data['state_key'].str() 229 | println('m.room.member user ${cmd.data['state_key']} room_id $room_id is ${c['membership']}') 230 | membership := c['membership'].str() 231 | match membership { 232 | 'invite' { 233 | room_alias := self.matrix.invited_by(cmd.data) 234 | println('matrix invite invited by $room_alias') 235 | match self.join_request_matrix(room_alias, room_id) { 236 | .saved { 237 | self.admin_say('recorded $room_id') 238 | self.sync_matrix_rooms() 239 | } 240 | .already_joined { 241 | self.admin_say('already joined $room_id') 242 | } 243 | } 244 | } 245 | 'join' { 246 | mut msg := '' 247 | if room := self.matrix.joined_rooms.find_room_by_id(room_id) { 248 | if room.name is string { 249 | room_name := room.name // VBUG 250 | msg = '$whoami joined ${room_name}.' 251 | if room_name.starts_with('@') { 252 | msg = msg + ' (DM room)' 253 | } else { 254 | if channel := bridge.matching_room(room_name, mut self.db) { 255 | msg = msg + ' (bridged to $channel)' 256 | } else { 257 | msg = msg + ' (not bridged. Use !bridge)' 258 | } 259 | } 260 | self.admin_say(msg) 261 | self.process_out_user(whoami) 262 | } 263 | } 264 | } 265 | 'leave' { 266 | mut msg := '$whoami left room ' 267 | if room := self.matrix.joined_rooms.find_room_by_id(room_id) { 268 | if room.name is string { 269 | msg = msg + room.name 270 | } else { 271 | msg = msg + '$room_id (found room, missing name)' 272 | } 273 | } else { 274 | msg = msg + '$room_id (unrecognized room)' 275 | } 276 | // self.admin_say(msg) 277 | } 278 | else {} 279 | } 280 | } 281 | 'm.room.message' { 282 | event_id := cmd.data['event_id'].str() 283 | c := cmd.data['content'].as_map() 284 | body := c['body'].str() 285 | room_id := cmd.data['room_id'].str() 286 | sender := cmd.data['sender'].str() 287 | if sender != self.matrix.whoami { 288 | println('<- $sender ${c['msgtype']} $room_id $event_id "$body"') 289 | // if not a puppet matrix user 290 | if _ := self.matrix_name_match(sender) { 291 | println('msg is from matrix puppet ${sender}. skipping') 292 | } else { 293 | // if room has a human name 294 | if alias_room := self.matrix.joined_rooms.by_id(room_id) { 295 | irc_nick := self.name_convert(chat.System.matrix, sender) 296 | if alias_room.name is string { 297 | if alias_room.name.starts_with('@') { 298 | if body.starts_with('!') { 299 | self.command(chat.System.matrix, 'matrix', sender, 300 | room_id, body[1..]) 301 | } 302 | } else { 303 | if room := bridge.matching_room(alias_room.name, mut self.db) { 304 | // repeat in irc 305 | saybody := match c['msgtype'].str() { 306 | 'm.text' { 307 | body 308 | } 309 | 'm.emote' { 310 | util.ctcp_encode('ACTION', body) 311 | } 312 | 'm.image' { 313 | self.media_announcement(c) 314 | } 315 | else { 316 | 'unknown matrix msgtype ${c['msgtype']}' 317 | } 318 | } 319 | room_parts := room.split(':') 320 | sayparts := saybody.split('\n') 321 | for saypart in sayparts { 322 | self.chat.say(chat.System.irc, irc_nick, room_parts[1], 323 | room_parts[0], saypart) 324 | } 325 | } else { 326 | println('as_do/m.room.message: no bridge found for ${alias_room}. msg dropped: $body') 327 | } 328 | } 329 | } else { 330 | println('warning: room_id $room_id is unrecognized. msg dropped: $body') 331 | } 332 | } 333 | } 334 | } else { 335 | println('$sender -> ${c['msgtype']} $room_id $event_id "$body"') 336 | } 337 | } 338 | else { 339 | println('unknown type') 340 | } 341 | } 342 | } 343 | 344 | fn (mut self Main) media_announcement(c map[string]json2.Any) string { 345 | // {"body":"IMG_20210704_154920.jpg","info":{"h":768,"mimetype":"image\/jpeg","size":163090,"w":1024},"msgtype":"m.image","url":"mxc:\/\/donp.org\/DBKlXYNItaxXzLDEgJwNdKBF"} 346 | media_url := self.matrix.mxc_to_url(c['url'].str()) 347 | return util.ctcp_encode('ACTION', 'uploaded $media_url') 348 | } 349 | 350 | fn (mut self Main) command(system chat.System, network string, name string, room_id string, message string) { 351 | parts := message.trim_space().split(' ') 352 | println('COMMAND $system $network $name, $room_id $parts') 353 | match parts[0] { 354 | 'help' { 355 | self.chat.say(system, '', network, room_id, 'commands: (each command gives its own help)') 356 | self.chat.say(system, '', network, room_id, '!status !irc !matrix !bridge') 357 | } 358 | 'status' { 359 | self.status_report(system, network, room_id) 360 | } 361 | 'matrix' { 362 | mut help_screen := false 363 | if parts.len > 1 { 364 | help_screen = self.command_matrix(system, network, room_id, parts) 365 | } else { 366 | help_screen = true 367 | } 368 | if help_screen { 369 | self.chat.say(system, '', network, room_id, '!matrix ') 370 | } 371 | } 372 | 'irc' { 373 | if parts.len > 1 { 374 | self.command_irc(system, name, network, room_id, parts) 375 | } else { 376 | self.chat.say(system, '', network, room_id, '!irc ') 377 | } 378 | } 379 | 'bridge' { 380 | mut help_screen := false 381 | if parts.len > 1 { 382 | help_screen = self.command_bridge(system, network, room_id, parts) 383 | } else { 384 | help_screen = true 385 | } 386 | if help_screen { 387 | self.chat.say(system, '', network, room_id, '!bridge ') 388 | } 389 | } 390 | else { 391 | self.chat.say(system, '', network, room_id, 'unknown command ${parts[0]}. try !help') 392 | } 393 | } 394 | } 395 | 396 | fn (mut self Main) status_report(system chat.System, network string, room string) { 397 | mut msg := '$self.matrix.host is $self.matrix.conn_state in $self.matrix.joined_rooms.len() rooms. $self.irc.networks.networks.len irc networks connected.' 398 | self.chat.say(system, '', network, room, msg) 399 | } 400 | 401 | fn (mut self Main) matrix_status(system chat.System, network string, room string) { 402 | mut msg := '$self.matrix.host is $self.matrix.conn_state in $self.matrix.joined_rooms.len() rooms. ' 403 | self.chat.say(system, '', network, room, msg) 404 | for r in self.matrix.joined_rooms.rooms { 405 | self.chat.say(system, '', network, room, 'matrix: room $r') 406 | } 407 | } 408 | 409 | fn (mut self Main) command_matrix(system chat.System, network string, room_id string, parts []string) bool { 410 | cmd := parts[1] 411 | match cmd { 412 | 'status' { 413 | self.matrix_status(system, network, room_id) 414 | } 415 | 'join' { 416 | if parts.len > 2 { 417 | room := parts[2] 418 | mut msg := '' 419 | if self.irc.is_room(room) { 420 | } else if self.matrix.is_room(room) { 421 | if alias_room := self.matrix.room_alias(room) { 422 | match self.join_request_matrix(room, alias_room.id) { 423 | .saved { 424 | msg = 'matrix room $room ($alias_room.id) added' 425 | go self.sync_matrix_rooms() 426 | } 427 | .already_joined { 428 | msg = 'matrix room $room already added' 429 | } 430 | } 431 | } else { 432 | match err { 433 | matrix.RoomAliasErrNotFound { 434 | msg = 'join failed: no room_alias exists for ${room}.' 435 | } 436 | else { 437 | msg = 'join failed: room_alias failure: ($err)' 438 | } 439 | } 440 | } 441 | } else { 442 | msg = 'join: unknown room format: $room' 443 | } 444 | self.chat.say(system, '', network, room_id, msg) 445 | } else { 446 | help_msg := '!join <#[:] | #>' 447 | self.chat.say(system, '', network, room_id, help_msg) 448 | self.chat.say(system, '', network, room_id, 'exmaple: !join #chatirc') 449 | self.chat.say(system, '', network, room_id, 'exmaple: !join #chat:matrix.org') 450 | } 451 | } 452 | 'leave' { 453 | if parts.len > 2 { 454 | room := parts[2] 455 | mut msg := '' 456 | if self.irc.is_room(room) { 457 | self.leave_request(chat.System.irc, network, room) 458 | msg = 'irc room $room removed' 459 | } else if self.matrix.is_room(room) { 460 | if alias_room := self.matrix.room_alias(room) { 461 | self.leave_request(chat.System.matrix, network, alias_room.id) 462 | msg = 'matrix room $alias_room removed' 463 | } else { 464 | match err { 465 | matrix.RoomAliasErrNotFound {} 466 | else {} 467 | } 468 | } 469 | } else { 470 | msg = 'leave: unknown room format: $room' 471 | } 472 | self.chat.say(system, '', network, room_id, msg) 473 | go self.sync_matrix_rooms() 474 | } else { 475 | self.chat.say(system, '', network, room_id, '!leave <#ircchannel or #matrix:room>') 476 | } 477 | } 478 | else { 479 | return true 480 | } 481 | } 482 | return false 483 | } 484 | 485 | fn (mut self Main) command_bridge(system chat.System, network string, room_id string, parts []string) bool { 486 | if parts.len > 1 { 487 | cmd := parts[1] 488 | match cmd { 489 | 'list' { 490 | rows := self.db.select_all('bridge_rooms') 491 | for row in rows { 492 | self.chat.say(system, '', network, room_id, 'bridge ${row[0]} <=> ${row[1]}') 493 | } 494 | } 495 | 'add' { 496 | if parts.len == 4 { 497 | left := parts[2] 498 | right := parts[3] 499 | matrix_server := left.split(':')[1] 500 | if matrix_server == self.config.matrix_host { 501 | irc_netname := right.split(':')[1] 502 | if _ := self.irc.networks.by_name(irc_netname) { 503 | bridge := bridge.Bridge{ 504 | left: left 505 | right: right 506 | } 507 | self.db.insert('bridge_rooms', bridge.to_db()) 508 | } else { 509 | self.chat.say(system, '', network, room_id, '$irc_netname is not a known irc network. use !irc add') 510 | } 511 | } else { 512 | self.chat.say(system, '', network, room_id, '$left must be a channel on $self.config.matrix_host') 513 | } 514 | } else { 515 | self.chat.say(system, '', network, room_id, '!bridge add #room:matrix.server #channel:ircnetwork') 516 | } 517 | } 518 | 'del' { 519 | if parts.len == 3 { 520 | self.db.delete_by_field('bridge_rooms', 'left', parts[2]) 521 | self.db.delete_by_field('bridge_rooms', 'right', parts[2]) 522 | } 523 | } 524 | else { 525 | return true 526 | } 527 | } 528 | return false 529 | } else { 530 | return true 531 | } 532 | } 533 | 534 | fn (mut self Main) command_irc(system chat.System, name string, network string, room_id string, parts []string) bool { 535 | mut default_action := false 536 | match parts[1] { 537 | 'add' { 538 | if parts.len == 4 { 539 | host := parts[2] 540 | nick := parts[3] 541 | mut irc_temp := irc.Network{ 542 | hostname: host 543 | nick: nick 544 | } 545 | self.db.insert('irc_servers', irc.network_to_db(irc_temp)) 546 | self.chat.say(system, '', network, room_id, 'host $host nick $nick added') 547 | // << copies ircnet, so find after add 548 | mut new_ircnet := self.irc.networks.add(irc_temp) 549 | mut bot_puppet := self.irc.new_ghost(mut new_ircnet, new_ircnet.nick) 550 | go bot_puppet.dial() 551 | } else { 552 | self.chat.say(system, '', network, room_id, 'usage: !irc add ') 553 | } 554 | } 555 | 'del' { 556 | if ircnet := self.irc.networks.by_name(parts[2]) { 557 | // VBUG if mut var := 558 | mut p_net := *ircnet 559 | self.db.delete_by_field('irc_servers', 'hostname', p_net.hostname) 560 | ircidx := self.irc.networks.by_hostname_idx(p_net.hostname) 561 | if ircidx >= 0 { 562 | mut puppets := self.irc.puppets.by_network(p_net.name) 563 | for mut puppet in puppets { 564 | puppet.hangup() 565 | } 566 | self.irc.networks.networks.delete(ircidx) 567 | } 568 | } else { 569 | println('server del $parts[2] not found') 570 | } 571 | } 572 | 'status' { 573 | for mut ircnet in self.irc.networks.networks { 574 | mut p_net := *ircnet 575 | self.chat.say(system, '', network, room_id, 'irc: network: $p_net') 576 | for mut puppet in self.irc.puppets.puppets { 577 | mut p_p := *puppet 578 | msg2 := '$p_net $p_p.nick $p_p.state $p_p.channels.channels.len channels' 579 | self.chat.say(system, '', network, room_id, 'irc: $msg2') 580 | for channel in p_p.channels.channels { 581 | msg3 := '$p_net $channel $p_p.nick' 582 | self.chat.say(system, '', network, room_id, 'irc: $msg3') 583 | } 584 | } 585 | } 586 | } 587 | 'nick' { 588 | if parts.len > 2 { 589 | if parts.len == 3 { 590 | nick := parts[2] 591 | old_nick := self.name_convert(chat.System.matrix, name) 592 | mut alias := self.aliases.match_matrix(name) or { 593 | alias := &chat.Alias{ 594 | matrix: name 595 | irc: nick 596 | } 597 | self.db.insert('bridge_nicks', chat.alias_to_db(alias)) 598 | self.aliases.add(alias) 599 | alias 600 | } 601 | if alias.irc != nick { 602 | self.chat.say(system, '', network, room_id, 'updated preferred irc nick for $name from $alias.irc to $nick') 603 | alias.irc = nick 604 | self.db.update_by_field('bridge_nicks', 'matrix', name, 'irc', 605 | nick) 606 | } 607 | mut puppet := self.irc.puppets.by_nick(alias.irc) or { 608 | d_msg := 'nick $alias.irc not found' 609 | self.chat.say(system, '', network, room_id, d_msg) 610 | return false 611 | } 612 | d_msg := 'nicksync $alias.irc == $puppet.nick' 613 | self.chat.say(system, '', network, room_id, d_msg) 614 | if old_nick == puppet.nick { 615 | msg := 'changing $puppet.network $puppet.nick to $nick' 616 | self.chat.say(system, '', network, room_id, msg) 617 | puppet.nick(nick) 618 | } 619 | } else { 620 | msg := '!irc nick ' 621 | self.chat.say(system, '', network, room_id, msg) 622 | } 623 | } else { 624 | mut p_msg := '' 625 | nick := self.name_convert(chat.System.matrix, name) 626 | p_msg = 'your ($name) preferred nick is $nick' 627 | self.chat.say(system, '', network, room_id, p_msg) 628 | for ircnet in self.irc.networks.networks { 629 | puppets := self.irc.puppets.by_network(ircnet.name) 630 | for puppet in puppets { 631 | p_msg = '$ircnet $puppet.nick' 632 | self.chat.say(system, '', network, room_id, p_msg) 633 | } 634 | } 635 | } 636 | } 637 | 'connect' { 638 | for mut puppet in self.irc.puppets.puppets { 639 | mut p_pup := *puppet 640 | mut msg := 'checking irc connection for $p_pup.nick' 641 | self.chat.say(system, '', network, room_id, msg) 642 | if p_pup.state == .disconnected { 643 | msg = '$p_pup.nick disconnected. reconnecting to $p_pup.network' 644 | self.chat.say(system, '', network, room_id, msg) 645 | p_pup.dial() 646 | println('command_irc connect p_pup.state $p_pup.state') 647 | if p_pup.state == .connected { 648 | self.irc.comm(mut p_pup) 649 | p_pup.signin() 650 | } 651 | } 652 | } 653 | } 654 | 'join' { 655 | if parts.len == 4 { 656 | mut msg := '' 657 | network_name := parts[2] 658 | room := parts[3] 659 | if join_network := self.irc.networks.by_name(network_name) { 660 | match self.join_request_irc(join_network.name, room) { 661 | .saved { 662 | msg = 'irc room $room added' 663 | irc_nick := self.name_convert(system, name) 664 | if ghost := self.irc.puppets.by_net_nick(join_network, irc_nick) { 665 | msg = msg + '(joining)' 666 | // vbug to use go self.sync_irc_channels 667 | self.sync_irc_channels(ghost) 668 | } else { 669 | msg = msg + 670 | '($irc_nick not currently connected to $join_network.name)' 671 | } 672 | } 673 | .already_joined { 674 | msg = 'irc room $room already added' 675 | } 676 | } 677 | } else { 678 | msg = 'no network named ${network_name}. use !irc list' 679 | } 680 | self.chat.say(system, '', network, room_id, msg) 681 | } else { 682 | self.chat.say(system, '', network, room_id, 'usage: !irc join <#channel>') 683 | } 684 | } 685 | 'part' { 686 | if parts.len == 4 { 687 | mut msg := '' 688 | network_name := parts[2] 689 | room := parts[3] 690 | if join_network := self.irc.networks.by_name(network_name) { 691 | self.leave_request(chat.System.irc, join_network.name, room) 692 | msg = 'irc room $room $join_network.name removed' 693 | } else { 694 | msg = 'no network named ${network_name}. use !irc list' 695 | } 696 | self.chat.say(system, '', network, room_id, msg) 697 | } else { 698 | self.chat.say(system, '', network, room_id, 'usage: !irc join <#channel>') 699 | } 700 | } 701 | else { 702 | default_action = true 703 | } 704 | } 705 | return default_action 706 | } 707 | 708 | fn (mut self Main) listen_out() { 709 | for { 710 | select { 711 | outmsg := <-self.chat.out { 712 | self.process_out(outmsg) 713 | } 714 | 1 * time.minute { 715 | if next_id := self.chat.queue.next_id() { 716 | println('listen_out timed out, found a msg.') 717 | self.process_out(next_id) 718 | } 719 | } 720 | } 721 | } 722 | } 723 | 724 | fn (mut self Main) process_out_user(user string) { 725 | println('process_out_user looking for $user') 726 | ids := self.chat.queue.by_name(user) 727 | for idx, id in ids { 728 | println('process_out_user found $user $id $idx/$ids.len') 729 | self.chat.out <- id 730 | break 731 | } 732 | } 733 | 734 | fn (mut self Main) process_out(id string) { 735 | if self.chat.queue.contains(id) { 736 | outmsg := self.chat.queue.get(id) 737 | println('process_out [$id/$self.chat.queue.len()] $outmsg') 738 | match outmsg.system { 739 | .irc { 740 | // TOOD remove 741 | ircnet_name := if outmsg.network.len == 0 { 742 | if self.irc.networks.networks.len > 0 { 743 | self.irc.networks.networks.first().hostname 744 | } else { 745 | 'noname' 746 | } 747 | } else { 748 | outmsg.network 749 | } 750 | match self.irc.say(ircnet_name, outmsg.name, outmsg.room, outmsg.message) { 751 | .good { 752 | println('process_out queue.delete($id)') 753 | self.chat.queue.delete(id) 754 | } 755 | .network_not_found {} 756 | .user_not_found { 757 | self.chat.cin <- chat.Payload(chat.MakeIrcUser{ 758 | network_hostname: ircnet_name 759 | nick: outmsg.name 760 | }) 761 | } 762 | .error { 763 | matrix_name := self.name_convert(chat.System.irc, outmsg.name) 764 | self.matrix_say(matrix_name, 'irc delivery failed $outmsg.room: $outmsg.message') 765 | } 766 | } 767 | } 768 | .matrix { 769 | if room := self.matrix.joined_rooms.find_room_by_id(outmsg.room) { 770 | if self.matrix.owner == outmsg.name { 771 | self.matrix.room_say(room, outmsg.message) 772 | } else { 773 | match self.matrix.room_say_as(outmsg.name, room, outmsg.message) { 774 | .good { 775 | self.chat.queue.delete(id) 776 | if self.chat.queue.len() > 0 { 777 | println('process_out finished ${id}. remaining queue len $self.chat.queue.len()') 778 | } 779 | } 780 | .user_not_found { 781 | nick := self.name_convert(chat.System.matrix, outmsg.name) 782 | self.matrix.cin <- matrix.Payload(matrix.MakeUser{ 783 | name: nick 784 | user_id: outmsg.name 785 | }) 786 | } 787 | .not_in_room { 788 | p := matrix.Payload(matrix.JoinRoom{ 789 | name: outmsg.name 790 | room: outmsg.room 791 | }) 792 | match self.matrix.cin.try_push(p) { 793 | .success {} 794 | .not_ready { println('WARNING matrix.cin channel not ready. $self.matrix.cin.len entries') } 795 | .closed {} 796 | } 797 | } 798 | .error { 799 | println('matrix room_say_as error. retainig msg for retransmission') 800 | } 801 | } 802 | } 803 | } else { 804 | println('listen_out matrix.joined_rooms.find_room_by_id failed: $err.msg() dropped: $outmsg.message ') 805 | println('process_out queue.delete($id)') 806 | self.chat.queue.delete(id) 807 | } 808 | } 809 | } 810 | } else { 811 | println('procesS_out dropping $id not in queue (len $self.chat.queue.len()') 812 | } 813 | } 814 | 815 | pub fn (mut self Main) leave_request(system chat.System, network string, room string) { 816 | table, field := match system { 817 | .irc { 818 | 'irc_channels', 'channel' 819 | } 820 | .matrix { 821 | 'matrix_rooms', 'room_id' 822 | } 823 | } 824 | self.db.delete_by_field(table, field, room) 825 | } 826 | 827 | enum JoinRequestResults { 828 | saved 829 | already_joined 830 | } 831 | 832 | pub fn (mut self Main) join_request_irc(network string, channel string) JoinRequestResults { 833 | self.db.insert('irc_channels', [ 834 | db.SqlValue{ name: 'channel', value: channel }, 835 | db.SqlValue{ 836 | name: 'netname' 837 | value: network 838 | }, 839 | ]) 840 | return JoinRequestResults.saved 841 | } 842 | 843 | pub fn (mut self Main) join_request_matrix(name string, room_id string) JoinRequestResults { 844 | self.db.insert('matrix_rooms', [db.SqlValue{ name: 'room_id', value: room_id }, 845 | db.SqlValue{ 846 | name: 'name' 847 | value: name 848 | }]) 849 | return JoinRequestResults.saved 850 | } 851 | 852 | pub fn (mut self Main) sync_matrix_rooms() { 853 | println('= sync_rooms') 854 | db_rooms := self.db.select_all('matrix_rooms').map(matrix.room_from_db(it)) 855 | println('sync_rooms: matrix db_rooms $db_rooms') 856 | println('sync_rooms: matrix joined_rooms $self.matrix.joined_rooms') 857 | needs_to_join := matrix.rooms_subtract(db_rooms, self.matrix.joined_rooms.pointers()) 858 | println('sync_rooms: matrix needs_to_join $needs_to_join') 859 | for room in needs_to_join { 860 | println('sync_rooms: joining $room.id') 861 | _, code := self.matrix.join(room.id) or { 862 | println('matrix.join fail $err') 863 | continue 864 | } 865 | if code == 200 { 866 | self.matrix.joined_rooms.add(room) 867 | self.admin_say('matrix: room $room joined') 868 | } else if code == 403 { 869 | println('sync_rooms: not invited to $room') 870 | self.leave_request(chat.System.matrix, '', room.id) 871 | } 872 | } 873 | for mut room in self.matrix.joined_rooms.rooms { 874 | if room.user_ids.len == 0 { 875 | room.user_ids = self.matrix.room_joined_members(room.id) or { continue } 876 | if room.name is string { 877 | if room.name.starts_with('@') { 878 | mut present := false 879 | for user_id in room.user_ids { 880 | if user_id == room.name { 881 | present = true 882 | } 883 | } 884 | if present == false { 885 | println('warning! $room does not include ${room.name}. sending invite.') 886 | self.matrix.room_invite(room, room.name) or {} 887 | } 888 | } 889 | } 890 | } 891 | } 892 | needs_to_leave := matrix.rooms_subtract(self.matrix.joined_rooms.pointers(), db_rooms) 893 | println('sync_rooms: matrix needs_to_leave $needs_to_leave') 894 | for room in needs_to_leave { 895 | self.matrix.leave(room.id) 896 | } 897 | } 898 | 899 | pub fn (mut self Main) sync_irc_channels(puppet irc.Puppet) { 900 | for row in self.db.select_all('irc_channels') { 901 | channel := row[0] 902 | netname := row[1] 903 | if puppet.network.name == netname { 904 | if puppet.state == .connected { 905 | if _ := puppet.find_channel(channel) { 906 | println('$puppet.network $puppet $channel already connected') 907 | } else { 908 | println('sync_irc_channels() $puppet.network $puppet.nick joining $netname $channel from db') 909 | mut puppet_mut := self.irc.puppets.by_nick(puppet.nick) or { continue } 910 | puppet_mut.join(channel) 911 | } 912 | } else { 913 | println('sync_irc_channels() $puppet.network $puppet.nick not connected. cannot join $channel') 914 | } 915 | } 916 | } 917 | } 918 | 919 | pub fn (mut self Main) matrix_name_match(name string) ?string { 920 | restr := '@$self.config.matrix_regex:$self.config.matrix_host' 921 | return regex_name_match(restr, name) 922 | } 923 | 924 | pub fn (mut self Main) irc_name_match(name string) bool { 925 | restr := self.config.irc_regex 926 | if rmatch := regex_name_match(restr, name) { 927 | println('irc_name_match $restr $name => $rmatch') 928 | return true 929 | } else { 930 | return false 931 | } 932 | } 933 | 934 | pub fn regex_name_match(restr string, name string) ?string { 935 | restr2 := restr.replace('|', '\\|') // pipe char helper hack 936 | mut re := regex.regex_opt(restr2) or { panic('regex_name_match regex parse fail for $restr') } 937 | _, _ := re.find(name) 938 | groups := re.get_group_list() 939 | group := groups[0] // always 1 group 940 | if group.end > 1 { 941 | return name[group.start..group.end] 942 | } else { 943 | return error('regex_name_match failed for $restr on $name') 944 | } 945 | } 946 | 947 | pub fn (mut self Main) name_convert(from_network chat.System, name string) string { 948 | return match from_network { 949 | .irc { 950 | alias := self.aliases.match_irc(name) or { 951 | matrix_user := regex_self_replace(self.config.matrix_regex, name) 952 | matrix_id := matrix.join([matrix_user, self.config.matrix_host]) 953 | alias := &chat.Alias{ 954 | matrix: matrix_id 955 | irc: name 956 | } 957 | self.matrix.user_displayname(matrix_id, name) or { 958 | println('ignoring user_displayname error for $matrix_id to $name') 959 | } 960 | self.aliases.add(alias) 961 | alias 962 | } 963 | alias.matrix 964 | } 965 | .matrix { 966 | alias := self.aliases.match_matrix(name) or { 967 | matrix_user := matrix.split(name)[1] 968 | irc_nick := regex_self_replace(self.config.irc_regex, matrix_user) 969 | alias := &chat.Alias{ 970 | matrix: name 971 | irc: irc_nick 972 | } 973 | // self.aliases.add(alias) 974 | alias 975 | } 976 | alias.irc 977 | } 978 | } 979 | } 980 | 981 | pub fn regex_self_replace(restr string, name string) string { 982 | newname := restr.replace_once('\(\.\*\)', name) 983 | println('regex_self_replace $restr $name => $newname') 984 | return newname 985 | } 986 | 987 | fn (mut self Main) nearest_matrix_channel(room_name string) ?string { 988 | if mroom := self.matrix.joined_rooms.room_by_partial_name(room_name.before(':')) { 989 | return mroom.id 990 | } else { 991 | return error('matchingmatrixchannelfail') 992 | } 993 | } 994 | 995 | fn (mut self Main) nearest_irc_channel(nick string, room &matrix.Room) ?string { 996 | if room.name is string { // VBUG: name := room.name { 997 | matrix_partial_name := room.name.before(':') 998 | println('matching_irc_channel matrix room name $room.name as irc channel name $matrix_partial_name') 999 | 1000 | mut sname := '' 1001 | // db search 1002 | rows := self.db.select_by_field('irc_channels', 'channel', matrix_partial_name) 1003 | if rows.len > 0 { 1004 | println('matching_irc_channel found $matrix_partial_name in irc_channels db') 1005 | sname = rows[0][0] 1006 | } else { 1007 | // connected channels search 1008 | if ircc := self.irc.find_channel_by_name(nick, matrix_partial_name) { 1009 | sname = ircc.name 1010 | } else { 1011 | return error('warning: matching_irc_channel found no irc channel for nick $nick matrix_room $matrix_partial_name') 1012 | } 1013 | } 1014 | return sname 1015 | } else { 1016 | return error('matching_irc_channel: giving up on room $room has no name') 1017 | } 1018 | } 1019 | 1020 | pub fn (mut self Main) admin_say(msg string) { 1021 | println('admin_say: $msg') 1022 | if self.config.admin_room.len > 0 { 1023 | if room := self.matrix.joined_rooms.find_room_by_name(self.config.admin_room) { 1024 | self.chat.say(chat.System.matrix, '', self.config.matrix_host, room.id, msg) 1025 | } else { 1026 | println('warning admin_say has no room ${self.config.admin_room}. dropping msg $msg') 1027 | } 1028 | } else { 1029 | self.matrix_say(self.config.matrix_owner, msg) 1030 | } 1031 | } 1032 | 1033 | pub fn (mut self Main) matrix_say(user string, msg string) { 1034 | mut room := matrix.Room{} 1035 | user_id := if user == self.matrix.whoami { self.config.matrix_owner } else { user } 1036 | if this_room := self.matrix.joined_rooms.dm(user_id) { 1037 | room = this_room 1038 | } else { 1039 | println('warning main.matrix_say has no private room with ${user_id}. creating room') 1040 | room = self.matrix.room_create(user_id) or { 1041 | println('main.matrix_say create_room $user_id failed. $err') 1042 | return 1043 | } 1044 | println('warning main.matrix_say created room ${room}. inviting $user_id') 1045 | self.matrix.room_invite(room, user_id) or { 1046 | println('main.matrix_say invite $room $user_id failed. $err') 1047 | return 1048 | } 1049 | } 1050 | self.chat.say(chat.System.matrix, '', self.config.matrix_host, room.id, msg) 1051 | } 1052 | --------------------------------------------------------------------------------