Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F77379
link.ex
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
8 KB
Subscribers
None
link.ex
View Options
defmodule
Nola.Plugins.Link
do
@moduledoc
"""
#
Link Previewer
An extensible link previewer for IRC.
To extend the supported sites, create a new handler implementing the callbacks.
See `link/` directory. The first in list handler that returns true to the `match/2` callback will be used,
and if the handler returns `:error` or crashes, will fallback to the default preview.
Unsupported websites will use the default link preview method, which is for html document the title, otherwise it'll use
the mimetype and size.
##
Configuration:
```
config :nola, Nola.Plugins.Link,
handlers: [
Nola.Plugins.Link.Youtube: [
invidious: true
],
Nola.Plugins.Link.Twitter: [],
Nola.Plugins.Link.Imgur: [],
]
```
"""
@ircdoc
"""
#
Link preview
Previews links (just post a link!).
Announces real URL after redirections and provides extended support for YouTube, Twitter and Imgur.
"""
def
short_irc_doc
,
do
:
false
def
irc_doc
,
do
:
@ircdoc
require
Logger
def
start_link
()
do
GenServer
.
start_link
(
__MODULE__
,
[],
name
:
__MODULE__
)
end
@callback
match
(
uri
::
URI
.
t
,
options
::
Keyword
.
t
)
::
{
true
,
params
::
Map
.
t
}
|
false
@callback
expand
(
uri
::
URI
.
t
,
params
::
Map
.
t
,
options
::
Keyword
.
t
)
::
{
:ok
,
lines
::
[]
|
String
.
t
}
|
:error
@callback
post_match
(
uri
::
URI
.
t
,
content_type
::
binary
,
headers
::
[],
opts
::
Keyword
.
t
)
::
{
:body
|
:file
,
params
::
Map
.
t
}
|
false
@callback
post_expand
(
uri
::
URI
.
t
,
body
::
binary
()
|
Path
.
t
,
params
::
Map
.
t
,
options
::
Keyword
.
t
)
::
{
:ok
,
lines
::
[]
|
String
.
t
}
|
:error
@optional_callbacks
[
expand
:
3
,
post_expand
:
4
]
defstruct
[
:client
]
def
init
([])
do
{
:ok
,
_
}
=
Registry
.
register
(
Nola.PubSub
,
"messages"
,
[
plugin
:
__MODULE__
])
#{:ok, _} = Registry.register(Nola.PubSub, "messages:telegram", [plugin: __MODULE__])
Logger
.
info
(
"Link handler started"
)
{
:ok
,
%
__MODULE__
{}}
end
def
handle_info
({
:irc
,
:text
,
message
=
%{
text
:
text
}},
state
)
do
String
.
split
(
text
)
|>
Enum
.
map
(
fn
(
word
)
->
if
String
.
starts_with?
(
word
,
"http://"
)
||
String
.
starts_with?
(
word
,
"https://"
)
do
uri
=
URI
.
parse
(
word
)
if
uri
.
scheme
&&
uri
.
host
do
spawn
(
fn
()
->
:timer
.
kill_after
(
:timer
.
seconds
(
30
))
case
expand_link
([
uri
])
do
{
:ok
,
uris
,
text
}
->
text
=
case
uris
do
[
uri
]
->
text
[
luri
|
_
]
->
if
luri
.
host
==
uri
.
host
&&
luri
.
path
==
luri
.
path
do
text
else
[
"->
#{
URI
.
to_string
(
luri
)
}
"
,
text
]
end
end
if
is_list
(
text
)
do
for
line
<-
text
,
do
:
message
.
replyfun
.
(
line
)
else
message
.
replyfun
.
(
text
)
end
_
->
nil
end
end
)
end
end
end
)
{
:noreply
,
state
}
end
def
handle_info
(
msg
,
state
)
do
{
:noreply
,
state
}
end
def
terminate
(
_reason
,
state
)
do
:ok
end
# 1. Match the first valid handler
# 2. Try to run the handler
# 3. If :error or crash, default link.
# If :skip, nothing
# 4. ?
# Over five redirections: cancel.
def
expand_link
(
acc
=
[
_
,
_
,
_
,
_
,
_
|
_
])
do
{
:ok
,
acc
,
"link redirects more than five times"
}
end
def
expand_link
(
acc
=
[
uri
|
_
])
do
Logger
.
debug
(
"link: expanding:
#{
inspect
uri
}
"
)
handlers
=
Keyword
.
get
(
Application
.
get_env
(
:nola
,
__MODULE__
,
[
handlers
:
[]]),
:handlers
)
handler
=
Enum
.
reduce_while
(
handlers
,
nil
,
fn
({
module
,
opts
},
acc
)
->
Logger
.
debug
(
"link: attempt expanding:
#{
inspect
module
}
for
#{
inspect
uri
}
"
)
module
=
Module
.
concat
([
module
])
case
module
.
match
(
uri
,
opts
)
do
{
true
,
params
}
->
{
:halt
,
{
module
,
params
,
opts
}}
false
->
{
:cont
,
acc
}
end
end
)
run_expand
(
acc
,
handler
)
end
def
run_expand
(
acc
,
nil
)
do
expand_default
(
acc
)
end
def
run_expand
(
acc
=
[
uri
|
_
],
{
module
,
params
,
opts
})
do
Logger
.
debug
(
"link: expanding
#{
inspect
uri
}
with
#{
inspect
module
}
"
)
case
module
.
expand
(
uri
,
params
,
opts
)
do
{
:ok
,
data
}
->
{
:ok
,
acc
,
data
}
:error
->
expand_default
(
acc
)
:skip
->
nil
end
rescue
e
->
Logger
.
error
(
"link: rescued
#{
inspect
uri
}
with
#{
inspect
module
}
:
#{
inspect
e
}
"
)
Logger
.
error
(
Exception
.
format
(
:error
,
e
,
__STACKTRACE__
))
expand_default
(
acc
)
catch
e
,
b
->
Logger
.
error
(
"link: catched
#{
inspect
uri
}
with
#{
inspect
module
}
:
#{
inspect
{
e
,
b
}
}
"
)
expand_default
(
acc
)
end
defp
get
(
url
,
headers
\\
[],
options
\\
[])
do
get_req
(
url
,
:hackney
.
get
(
url
,
headers
,
<<>>,
options
))
end
defp
get_req
(
_
,
{
:error
,
reason
})
do
{
:error
,
reason
}
end
defp
get_req
(
url
,
{
:ok
,
200
,
headers
,
client
})
do
headers
=
Enum
.
reduce
(
headers
,
%{},
fn
({
key
,
value
},
acc
)
->
Map
.
put
(
acc
,
String
.
downcase
(
key
),
value
)
end
)
content_type
=
Map
.
get
(
headers
,
"content-type"
,
"application/octect-stream"
)
length
=
Map
.
get
(
headers
,
"content-length"
,
"0"
)
{
length
,
_
}
=
Integer
.
parse
(
length
)
handlers
=
Keyword
.
get
(
Application
.
get_env
(
:nola
,
__MODULE__
,
[
handlers
:
[]]),
:handlers
)
handler
=
Enum
.
reduce_while
(
handlers
,
false
,
fn
({
module
,
opts
},
acc
)
->
module
=
Module
.
concat
([
module
])
try
do
case
module
.
post_match
(
url
,
content_type
,
headers
,
opts
)
do
{
mode
,
params
}
when
mode
in
[
:body
,
:file
]
->
{
:halt
,
{
module
,
params
,
opts
,
mode
}}
false
->
{
:cont
,
acc
}
end
rescue
e
->
Logger
.
error
(
inspect
(
e
))
{
:cont
,
false
}
catch
e
,
b
->
Logger
.
error
(
inspect
({
b
}))
{
:cont
,
false
}
end
end
)
cond
do
handler
!=
false
and
length
<=
30_000_000
->
case
get_body
(
url
,
30_000_000
,
client
,
handler
,
<<>>)
do
{
:ok
,
_
}
=
ok
->
ok
:error
->
{
:ok
,
"file:
#{
content_type
}
, size:
#{
human_size
(
length
)
}
"
}
end
#String.starts_with?(content_type, "text/html") && length <= 30_000_000 ->
# get_body(url, 30_000_000, client, <<>>)
true
->
:hackney
.
close
(
client
)
{
:ok
,
"file:
#{
content_type
}
, size:
#{
human_size
(
length
)
}
"
}
end
end
defp
get_req
(
_
,
{
:ok
,
redirect
,
headers
,
client
})
when
redirect
in
300
..
399
do
headers
=
Enum
.
reduce
(
headers
,
%{},
fn
({
key
,
value
},
acc
)
->
Map
.
put
(
acc
,
String
.
downcase
(
key
),
value
)
end
)
location
=
Map
.
get
(
headers
,
"location"
)
:hackney
.
close
(
client
)
{
:redirect
,
location
}
end
defp
get_req
(
_
,
{
:ok
,
status
,
headers
,
client
})
do
:hackney
.
close
(
client
)
{
:error
,
status
,
headers
}
end
defp
get_body
(
url
,
len
,
client
,
{
handler
,
params
,
opts
,
mode
}
=
h
,
acc
)
when
len
>=
byte_size
(
acc
)
do
case
:hackney
.
stream_body
(
client
)
do
{
:ok
,
data
}
->
get_body
(
url
,
len
,
client
,
h
,
<<
acc
::
binary
,
data
::
binary
>>)
:done
->
body
=
case
mode
do
:body
->
acc
:file
->
{
:ok
,
tmpfile
}
=
Plug.Upload
.
random_file
(
"linkplugin"
)
File
.
write!
(
tmpfile
,
acc
)
tmpfile
end
handler
.
post_expand
(
url
,
body
,
params
,
opts
)
{
:error
,
reason
}
->
{
:ok
,
"failed to fetch body:
#{
inspect
reason
}
"
}
end
end
defp
get_body
(
_
,
len
,
client
,
h
,
_acc
)
do
:hackney
.
close
(
client
)
IO
.
inspect
(
h
)
{
:ok
,
"Error: file over 30"
}
end
def
expand_default
(
acc
=
[
uri
=
%
URI
{
scheme
:
scheme
}
|
_
])
when
scheme
in
[
"http"
,
"https"
]
do
Logger
.
debug
(
"link: expanding
#{
uri
}
with default"
)
headers
=
[{
"user-agent"
,
"DmzBot (like TwitterBot)"
}]
options
=
[
follow_redirect
:
false
,
max_body_length
:
30_000_000
]
case
get
(
URI
.
to_string
(
uri
),
headers
,
options
)
do
{
:ok
,
text
}
->
{
:ok
,
acc
,
text
}
{
:redirect
,
link
}
->
new_uri
=
URI
.
parse
(
link
)
#new_uri = %URI{new_uri | scheme: scheme, authority: uri.authority, host: uri.host, port: uri.port}
expand_link
([
new_uri
|
acc
])
{
:error
,
status
,
_headers
}
->
text
=
Plug.Conn.Status
.
reason_phrase
(
status
)
{
:ok
,
acc
,
"Error: HTTP
#{
text
}
(
#{
status
}
)"
}
{
:error
,
{
:tls_alert
,
{
:handshake_failure
,
err
}}}
->
{
:ok
,
acc
,
"TLS Error:
#{
to_string
(
err
)
}
"
}
{
:error
,
reason
}
->
{
:ok
,
acc
,
"Error:
#{
to_string
(
reason
)
}
"
}
end
end
# Unsupported scheme, came from a redirect.
def
expand_default
(
acc
=
[
uri
|
_
])
do
{
:ok
,
[
uri
],
"->
#{
URI
.
to_string
(
uri
)
}
"
}
end
defp
human_size
(
bytes
)
do
bytes
|>
FileSize
.
new
(
:b
)
|>
FileSize
.
scale
()
|>
FileSize
.
format
()
end
end
File Metadata
Details
Attached
Mime Type
text/x-ruby
Expires
Mon, Jul 7, 7:29 AM (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
49872
Default Alt Text
link.ex (8 KB)
Attached To
rNOLA Nola
Event Timeline
Log In to Comment