Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F652921
connection.ex
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
16 KB
Subscribers
None
connection.ex
View Options
defmodule
IRC.Connection
do
require
Logger
use
Ecto.Schema
@moduledoc
"""
#
IRC Connection
Provides a nicer abstraction over ExIRC's handlers.
##
Start connections
```
IRC.Connection.start_link(host: "irc.random.sh", port: 6697, nick: "pouetbot", channels: ["
#
dev"])
##
PubSub topics
* `account` -- accounts change
* {:account_change, old_account_id, new_account_id}
#
Sent when account merged
* {:accounts, [{:account, network, channel, nick, account_id}]
#
Sent on bot join
* {:account, network, nick, account_id}
#
Sent on user join
* `message` -- aill messages (without triggers)
* `message:private` -- all messages without a channel
* `message:
#
CHANNEL` -- all messages within `
#
CHANNEL`
* `triggers` -- all triggers
* `trigger:TRIGGER` -- any message with a trigger `TRIGGER`
##
Replying to %IRC.Message{}
Each `IRC.Message` comes with a dedicated `replyfun`, to which you only have to pass either:
"""
def
irc_doc
,
do
:
nil
@min_backoff
:timer
.
seconds
(
5
)
@max_backoff
:timer
.
seconds
(
2
*
60
)
embedded_schema
do
field
:network
,
:string
field
:host
,
:string
field
:port
,
:integer
field
:nick
,
:string
field
:user
,
:string
field
:name
,
:string
field
:pass
,
:string
field
:tls
,
:boolean
,
default
:
false
field
:channels
,
{
:array
,
:string
},
default
:
[]
end
defmodule
Supervisor
do
use
DynamicSupervisor
def
start_link
()
do
DynamicSupervisor
.
start_link
(
__MODULE__
,
[],
name
:
__MODULE__
)
end
def
start_child
(%
IRC.Connection
{}
=
conn
)
do
spec
=
%{
id
:
conn
.
id
,
start
:
{
IRC.Connection
,
:start_link
,
[
conn
]},
restart
:
:transient
}
DynamicSupervisor
.
start_child
(
__MODULE__
,
spec
)
end
@impl
true
def
init
(
_init_arg
)
do
DynamicSupervisor
.
init
(
strategy
:
:one_for_one
,
max_restarts
:
10
,
max_seconds
:
1
)
end
end
def
changeset
(
params
)
do
import
Ecto.Changeset
%
__MODULE__
{
id
:
EntropyString
.
large_id
()}
|>
cast
(
params
,
[
:network
,
:host
,
:port
,
:nick
,
:user
,
:name
,
:pass
,
:channels
,
:tls
])
|>
validate_required
([
:host
,
:port
,
:nick
,
:user
,
:name
])
|>
apply_action
(
:insert
)
end
def
to_tuple
(
%
__MODULE__
{}
=
conn
)
do
{
conn
.
id
,
conn
.
network
,
conn
.
host
,
conn
.
port
,
conn
.
nick
,
conn
.
user
,
conn
.
name
,
conn
.
pass
,
conn
.
tls
,
conn
.
channels
,
nil
}
end
def
from_tuple
({
id
,
network
,
host
,
port
,
nick
,
user
,
name
,
pass
,
tls
,
channels
,
_
})
do
%
__MODULE__
{
id
:
id
,
network
:
network
,
host
:
host
,
port
:
port
,
nick
:
nick
,
user
:
user
,
name
:
name
,
pass
:
pass
,
tls
:
tls
,
channels
:
channels
}
end
## -- MANAGER API
def
setup
()
do
:dets
.
open_file
(
dets
(),
[])
end
def
dets
(),
do
:
to_charlist
(
Nola
.
data_path
(
"/connections.dets"
))
def
lookup
(
id
)
do
case
:dets
.
lookup
(
dets
(),
id
)
do
[
object
|
_
]
->
from_tuple
(
object
)
_
->
nil
end
end
def
connections
()
do
:dets
.
foldl
(
fn
(
object
,
acc
)
->
[
from_tuple
(
object
)
|
acc
]
end
,
[],
dets
())
end
def
start_all
()
do
for
conn
<-
connections
(),
do
:
{
conn
,
IRC.Connection.Supervisor
.
start_child
(
conn
)}
end
def
get_network
(
network
,
channel
\\
nil
)
do
spec
=
[{{
:_
,
:"$1"
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
},
[{
:==
,
:"$1"
,
{
:const
,
network
}}],
[
:"$_"
]}]
results
=
Enum
.
map
(
:dets
.
select
(
dets
(),
spec
),
fn
(
object
)
->
from_tuple
(
object
)
end
)
if
channel
do
Enum
.
find
(
results
,
fn
(
conn
)
->
Enum
.
member?
(
conn
.
channels
,
channel
)
end
)
else
List
.
first
(
results
)
end
end
def
get_host_nick
(
host
,
port
,
nick
)
do
spec
=
[{{
:_
,
:_
,
:"$1"
,
:"$2"
,
:"$3"
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
},
[{
:andalso
,
{
:andalso
,
{
:==
,
:"$1"
,
{
:const
,
host
}},
{
:==
,
:"$2"
,
{
:const
,
port
}}},
{
:==
,
:"$3"
,
{
:const
,
nick
}}}],
[
:"$_"
]}
]
case
:dets
.
select
(
dets
(),
spec
)
do
[
object
]
->
from_tuple
(
object
)
[]
->
nil
end
end
def
delete_connection
(
%
__MODULE__
{
id
:
id
}
=
conn
)
do
:dets
.
delete
(
dets
(),
id
)
stop_connection
(
conn
)
:ok
end
def
start_connection
(
%
__MODULE__
{}
=
conn
)
do
IRC.Connection.Supervisor
.
start_child
(
conn
)
end
def
stop_connection
(
%
__MODULE__
{
id
:
id
})
do
case
:global
.
whereis_name
(
id
)
do
pid
when
is_pid
(
pid
)
->
GenServer
.
stop
(
pid
,
:normal
)
_
->
:error
end
end
def
add_connection
(
opts
)
do
case
changeset
(
opts
)
do
{
:ok
,
conn
}
->
if
existing
=
get_host_nick
(
conn
.
host
,
conn
.
port
,
conn
.
nick
)
do
{
:error
,
{
:existing
,
conn
}}
else
:dets
.
insert
(
dets
(),
to_tuple
(
conn
))
IRC.Connection.Supervisor
.
start_child
(
conn
)
end
error
->
error
end
end
def
update_connection
(
connection
)
do
:dets
.
insert
(
dets
(),
to_tuple
(
connection
))
end
def
start_link
(
conn
)
do
GenServer
.
start_link
(
__MODULE__
,
[
conn
],
name
:
{
:global
,
conn
.
id
})
end
def
broadcast_message
(
net
,
chan
,
message
)
do
dispatch
(
"conn"
,
{
:broadcast
,
net
,
chan
,
message
},
IRC.ConnectionPubSub
)
end
def
broadcast_message
(
list
,
message
)
when
is_list
(
list
)
do
for
{
net
,
chan
}
<-
list
do
broadcast_message
(
net
,
chan
,
message
)
end
end
def
privmsg
(
channel
,
line
)
do
GenServer
.
cast
(
__MODULE__
,
{
:privmsg
,
channel
,
line
})
end
def
init
([
conn
])
do
Logger
.
metadata
(
conn
:
conn
.
id
)
backoff
=
:backoff
.
init
(
@min_backoff
,
@max_backoff
)
|>
:backoff
.
type
(
:jitter
)
{
:ok
,
%{
client
:
nil
,
backoff
:
backoff
,
conn
:
conn
,
connected_server
:
nil
,
connected_port
:
nil
,
network
:
conn
.
network
},
{
:continue
,
:connect
}}
end
@triggers
%{
"!"
=>
:bang
,
"+"
=>
:plus
,
"-"
=>
:minus
,
"?"
=>
:query
,
"."
=>
:dot
,
"~"
=>
:tilde
,
"@"
=>
:at
,
"++"
=>
:plus_plus
,
"--"
=>
:minus_minus
,
"!!"
=>
:bang_bang
,
"??"
=>
:query_query
,
".."
=>
:dot_dot
,
"~~"
=>
:tilde_tilde
,
"@@"
=>
:at_at
}
def
handle_continue
(
:connect
,
state
)
do
client_opts
=
[]
|>
Keyword
.
put
(
:network
,
state
.
conn
.
network
)
{
:ok
,
_
}
=
Registry
.
register
(
IRC.ConnectionPubSub
,
"conn"
,
[])
client
=
if
state
.
client
&&
Process
.
alive?
(
state
.
client
)
do
Logger
.
info
(
"Reconnecting client"
)
state
.
client
else
Logger
.
info
(
"Connecting"
)
{
:ok
,
client
}
=
ExIRC.Client
.
start_link
(
debug
:
false
)
ExIRC.Client
.
add_handler
(
client
,
self
())
client
end
opts
=
[{
:nodelay
,
true
}]
conn_fun
=
if
state
.
conn
.
tls
,
do
:
:connect_ssl!
,
else
:
:connect!
apply
(
ExIRC.Client
,
conn_fun
,
[
client
,
to_charlist
(
state
.
conn
.
host
),
state
.
conn
.
port
,
opts
])
{
:noreply
,
%{
state
|
client
:
client
}}
end
def
handle_info
(
:disconnected
,
state
)
do
{
delay
,
backoff
}
=
:backoff
.
fail
(
state
.
backoff
)
Logger
.
info
(
"
#{
inspect
(
self
())
}
Disconnected -- reconnecting in
#{
inspect
delay
}
ms"
)
Process
.
send_after
(
self
(),
:connect
,
delay
)
{
:noreply
,
%{
state
|
backoff
:
backoff
}}
end
def
handle_info
(
:connect
,
state
)
do
{
:noreply
,
state
,
{
:continue
,
:connect
}}
end
def
handle_cast
({
:privmsg
,
channel
,
line
},
state
)
do
irc_reply
(
state
,
{
channel
,
nil
},
line
)
{
:noreply
,
state
}
end
# Connection successful
def
handle_info
({
:connected
,
server
,
port
},
state
)
do
Logger
.
info
(
"
#{
inspect
(
self
())
}
Connected to
#{
inspect
(
server
)
}
:
#{
port
}
#{
inspect
state
}
"
)
{
_
,
backoff
}
=
:backoff
.
succeed
(
state
.
backoff
)
ExIRC.Client
.
logon
(
state
.
client
,
state
.
conn
.
pass
||
""
,
state
.
conn
.
nick
,
state
.
conn
.
user
,
state
.
conn
.
name
)
{
:noreply
,
%{
state
|
backoff
:
backoff
,
connected_server
:
server
,
connected_port
:
port
}}
end
# Logon successful
def
handle_info
(
:logged_in
,
state
)
do
Logger
.
info
(
"
#{
inspect
(
self
())
}
Logged in"
)
{
_
,
backoff
}
=
:backoff
.
succeed
(
state
.
backoff
)
Enum
.
map
(
state
.
conn
.
channels
,
&
ExIRC.Client
.
join
(
state
.
client
,
&1
))
{
:noreply
,
%{
state
|
backoff
:
backoff
}}
end
# ISUP
def
handle_info
({
:isup
,
network
},
state
)
when
is_binary
(
network
)
do
IRC.UserTrack
.
clear_network
(
state
.
network
)
if
network
!=
state
.
network
do
Logger
.
warn
(
"Possibly misconfigured network:
#{
network
}
!=
#{
state
.
network
}
"
)
end
{
:noreply
,
state
}
end
# Been kicked
def
handle_info
({
:kicked
,
_sender
,
chan
,
_reason
},
state
)
do
ExIRC.Client
.
join
(
state
.
client
,
chan
)
{
:noreply
,
state
}
end
# Received something in a channel
def
handle_info
({
:received
,
text
,
sender
,
chan
},
state
)
do
user
=
if
user
=
IRC.UserTrack
.
find_by_nick
(
state
.
network
,
sender
.
nick
)
do
user
else
Logger
.
error
(
"Could not lookup user for message:
#{
inspect
{
state
.
network
,
chan
,
sender
.
nick
}
}
"
)
user
=
IRC.UserTrack
.
joined
(
chan
,
sender
,
[])
ExIRC.Client
.
who
(
state
.
client
,
chan
)
# Rewho everything in case of need ? We shouldn't not know that user..
user
end
if
!
user
do
ExIRC.Client
.
who
(
state
.
client
,
chan
)
# Rewho everything in case of need ? We shouldn't not know that user..
Logger
.
error
(
"Could not lookup user nor create it for message:
#{
inspect
{
state
.
network
,
chan
,
sender
.
nick
}
}
"
)
else
if
!
Map
.
get
(
user
.
options
,
:puppet
)
do
reply_fun
=
fn
(
text
)
->
irc_reply
(
state
,
{
chan
,
sender
},
text
)
end
account
=
IRC.Account
.
lookup
(
sender
)
message
=
%
IRC.Message
{
id
:
FlakeId
.
get
(),
transport
:
:irc
,
at
:
NaiveDateTime
.
utc_now
(),
text
:
text
,
network
:
state
.
network
,
account
:
account
,
sender
:
sender
,
channel
:
chan
,
replyfun
:
reply_fun
,
trigger
:
extract_trigger
(
text
)}
message
=
case
IRC.UserTrack
.
messaged
(
message
)
do
:ok
->
message
{
:ok
,
message
}
->
message
end
publish
(
message
,
[
"
#{
message
.
network
}
/
#{
chan
}
:messages"
])
end
end
{
:noreply
,
state
}
end
# Received a private message
def
handle_info
({
:received
,
text
,
sender
},
state
)
do
reply_fun
=
fn
(
text
)
->
irc_reply
(
state
,
{
sender
.
nick
,
sender
},
text
)
end
account
=
IRC.Account
.
lookup
(
sender
)
message
=
%
IRC.Message
{
id
:
FlakeId
.
get
(),
transport
:
:irc
,
text
:
text
,
network
:
state
.
network
,
at
:
NaiveDateTime
.
utc_now
(),
account
:
account
,
sender
:
sender
,
replyfun
:
reply_fun
,
trigger
:
extract_trigger
(
text
)}
message
=
case
IRC.UserTrack
.
messaged
(
message
)
do
:ok
->
message
{
:ok
,
message
}
->
message
end
publish
(
message
,
[
"messages:private"
,
"
#{
message
.
network
}
/
#{
account
.
id
}
:messages"
])
{
:noreply
,
state
}
end
## -- Broadcast
def
handle_info
({
:broadcast
,
net
,
account
=
%
IRC.Account
{},
message
},
state
)
do
if
net
==
state
.
conn
.
network
do
user
=
IRC.UserTrack
.
find_by_account
(
net
,
account
)
if
user
do
irc_reply
(
state
,
{
user
.
nick
,
nil
},
message
)
end
end
{
:noreply
,
state
}
end
def
handle_info
({
:broadcast
,
net
,
chan
,
message
},
state
)
do
if
net
==
state
.
conn
.
network
&&
Enum
.
member?
(
state
.
conn
.
channels
,
chan
)
do
irc_reply
(
state
,
{
chan
,
nil
},
message
)
end
{
:noreply
,
state
}
end
## -- UserTrack
def
handle_info
({
:joined
,
channel
},
state
)
do
ExIRC.Client
.
who
(
state
.
client
,
channel
)
{
:noreply
,
state
}
end
def
handle_info
({
:who
,
channel
,
whos
},
state
)
do
accounts
=
Enum
.
map
(
whos
,
fn
(
who
=
%
ExIRC.Who
{
nick
:
nick
,
operator?
:
operator
})
->
priv
=
if
operator
,
do
:
[
:operator
],
else
:
[]
# Don't touch -- on WHO the bot joined, not the users.
IRC.UserTrack
.
joined
(
channel
,
who
,
priv
,
false
)
account
=
IRC.Account
.
lookup
(
who
)
if
account
do
{
:account
,
who
.
network
,
channel
,
who
.
nick
,
account
.
id
}
end
end
)
|>
Enum
.
filter
(
fn
(
x
)
->
x
end
)
dispatch
(
"account"
,
{
:accounts
,
accounts
})
{
:noreply
,
state
}
end
def
handle_info
({
:quit
,
reason
,
sender
},
state
)
do
IRC.UserTrack
.
quitted
(
sender
,
reason
)
{
:noreply
,
state
}
end
def
handle_info
({
:joined
,
channel
,
sender
},
state
)
do
IRC.UserTrack
.
joined
(
channel
,
sender
,
[])
account
=
IRC.Account
.
lookup
(
sender
)
if
account
do
dispatch
(
"account"
,
{
:account
,
sender
.
network
,
channel
,
sender
.
nick
,
account
.
id
})
end
{
:noreply
,
state
}
end
def
handle_info
({
:kicked
,
nick
,
_by
,
channel
,
_reason
},
state
)
do
IRC.UserTrack
.
parted
(
state
.
network
,
channel
,
nick
)
{
:noreply
,
state
}
end
def
handle_info
({
:parted
,
channel
,
%
ExIRC.SenderInfo
{
nick
:
nick
}},
state
)
do
IRC.UserTrack
.
parted
(
state
.
network
,
channel
,
nick
)
{
:noreply
,
state
}
end
def
handle_info
({
:mode
,
[
channel
,
mode
,
nick
]},
state
)
do
track_mode
(
state
.
network
,
channel
,
nick
,
mode
)
{
:noreply
,
state
}
end
def
handle_info
({
:nick_changed
,
old_nick
,
new_nick
},
state
)
do
IRC.UserTrack
.
renamed
(
state
.
network
,
old_nick
,
new_nick
)
{
:noreply
,
state
}
end
def
handle_info
(
unhandled
,
client
)
do
Logger
.
debug
(
"unhandled:
#{
inspect
unhandled
}
"
)
{
:noreply
,
client
}
end
def
publish
(
pub
),
do
:
publish
(
pub
,
[])
def
publish
(
m
=
%
IRC.Message
{
trigger
:
nil
},
keys
)
do
dispatch
([
"messages"
]
++
keys
,
{
:irc
,
:text
,
m
})
end
def
publish
(
m
=
%
IRC.Message
{
trigger
:
t
=
%
IRC.Trigger
{
trigger
:
trigger
}},
keys
)
do
dispatch
([
"triggers"
,
"
#{
m
.
network
}
/
#{
m
.
channel
}
:triggers"
,
"trigger:"
<>
trigger
],
{
:irc
,
:trigger
,
trigger
,
m
})
end
def
publish_event
(
net
,
event
=
%{
type
:
_
})
when
is_binary
(
net
)
do
event
=
event
|>
Map
.
put
(
:at
,
NaiveDateTime
.
utc_now
())
|>
Map
.
put
(
:network
,
net
)
dispatch
(
"
#{
net
}
:events"
,
{
:irc
,
:event
,
event
})
end
def
publish_event
({
net
,
chan
},
event
=
%{
type
:
type
})
do
event
=
event
|>
Map
.
put
(
:at
,
NaiveDateTime
.
utc_now
())
|>
Map
.
put
(
:network
,
net
)
|>
Map
.
put
(
:channel
,
chan
)
dispatch
(
"
#{
net
}
/
#{
chan
}
:events"
,
{
:irc
,
:event
,
event
})
end
def
dispatch
(
keys
,
content
,
sub
\\
IRC.PubSub
)
def
dispatch
(
key
,
content
,
sub
)
when
is_binary
(
key
),
do
:
dispatch
([
key
],
content
,
sub
)
def
dispatch
(
keys
,
content
,
sub
)
when
is_list
(
keys
)
do
Logger
.
debug
(
"dispatch
#{
inspect
keys
}
=
#{
inspect
content
}
"
)
for
key
<-
keys
do
spawn
(
fn
()
->
Registry
.
dispatch
(
sub
,
key
,
fn
h
->
for
{
pid
,
_
}
<-
h
,
do
:
send
(
pid
,
content
)
end
)
end
)
end
end
#
# Triggers
#
def
triggers
,
do
:
@triggers
for
{
trigger
,
name
}
<-
@triggers
do
def
extract_trigger
(
unquote
(
trigger
)
<>
text
)
do
text
=
String
.
strip
(
text
)
[
trigger
|
args
]
=
String
.
split
(
text
,
" "
)
%
IRC.Trigger
{
type
:
unquote
(
name
),
trigger
:
String
.
downcase
(
trigger
),
args
:
args
}
end
end
def
extract_trigger
(
_
),
do
:
nil
#
# IRC Replies
#
# irc_reply(ExIRC.Client pid, {channel or nick, ExIRC.Sender}, binary | replies
# replies :: {:kick, reason} | {:kick, nick, reason} | {:mode, mode, nick}
defp
irc_reply
(
state
=
%{
client
:
client
,
network
:
network
},
{
target
,
_
},
text
)
when
is_binary
(
text
)
or
is_list
(
text
)
do
lines
=
IRC
.
splitlong
(
text
)
|>
Enum
.
map
(
fn
(
x
)
->
if
(
is_list
(
x
),
do
:
x
,
else
:
String
.
split
(
x
,
"
\n
"
))
end
)
|>
List
.
flatten
()
outputs
=
for
line
<-
lines
do
ExIRC.Client
.
msg
(
client
,
:privmsg
,
target
,
line
)
{
:irc
,
:out
,
%
IRC.Message
{
id
:
FlakeId
.
get
(),
transport
:
:irc
,
network
:
network
,
channel
:
target
,
text
:
line
,
sender
:
%
ExIRC.SenderInfo
{
nick
:
state
.
conn
.
nick
},
at
:
NaiveDateTime
.
utc_now
(),
meta
:
%{
self
:
true
}}}
end
for
f
<-
outputs
,
do
:
dispatch
([
"irc:outputs"
,
"
#{
network
}
/
#{
target
}
:outputs"
],
f
)
end
defp
irc_reply
(%{
client
:
client
},
{
target
,
%{
nick
:
nick
}},
{
:kick
,
reason
})
do
ExIRC.Client
.
kick
(
client
,
target
,
nick
,
reason
)
end
defp
irc_reply
(%{
client
:
client
},
{
target
,
_
},
{
:kick
,
nick
,
reason
})
do
ExIRC.Client
.
kick
(
client
,
target
,
nick
,
reason
)
end
defp
irc_reply
(%{
client
:
client
},
{
target
,
%{
nick
:
nick
}},
{
:mode
,
mode
})
do
ExIRC.Client
.
mode
(%{
client
:
client
},
target
,
mode
,
nick
)
end
defp
irc_reply
(%{
client
:
client
},
target
,
{
:mode
,
mode
,
nick
})
do
ExIRC.Client
.
mode
(
client
,
target
,
mode
,
nick
)
end
defp
irc_reply
(%{
client
:
client
},
target
,
{
:channel_mode
,
mode
})
do
ExIRC.Client
.
mode
(
client
,
target
,
mode
)
end
defp
track_mode
(
network
,
channel
,
nick
,
"+o"
)
do
IRC.UserTrack
.
change_privileges
(
network
,
channel
,
nick
,
{[
:operator
],
[]})
:ok
end
defp
track_mode
(
network
,
channel
,
nick
,
"-o"
)
do
IRC.UserTrack
.
change_privileges
(
network
,
channel
,
nick
,
{[],
[
:operator
]})
:ok
end
defp
track_mode
(
network
,
channel
,
nick
,
"+v"
)
do
IRC.UserTrack
.
change_privileges
(
network
,
channel
,
nick
,
{[
:voice
],
[]})
:ok
end
defp
track_mode
(
network
,
channel
,
nick
,
"-v"
)
do
IRC.UserTrack
.
change_privileges
(
network
,
channel
,
nick
,
{[],
[
:voice
]})
:ok
end
defp
track_mode
(
network
,
channel
,
nick
,
mode
)
do
Logger
.
warn
(
"Unhandled track_mode:
#{
inspect
{
nick
,
mode
}
}
"
)
:ok
end
defp
server
(%{
conn
:
%{
host
:
host
,
port
:
port
}})
do
host
<>
":"
<>
to_string
(
port
)
end
end
File Metadata
Details
Attached
Mime Type
text/x-ruby
Expires
Fri, Feb 6, 12:12 AM (6 h, 37 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
82575
Default Alt Text
connection.ex (16 KB)
Attached To
rNOLA Nola
Event Timeline
Log In to Comment