Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F665291
link_plugin.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_plugin.ex
View Options
defmodule
LSG.IRC.LinkPlugin
do
@moduledoc
"""
#
Link Previewer
An extensible link previewer for IRC.
To extend the supported sites, create a new handler implementing the callbacks.
See `link_plugin/` directory for examples. 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 :lsg, LSG.IRC.LinkPlugin,
handlers: [
LSG.IRC.LinkPlugin.Youtube: [
invidious: true
],
LSG.IRC.LinkPlugin.Twitter: [],
LSG.IRC.LinkPlugin.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
(
IRC.PubSub
,
"messages"
,
[
plugin
:
__MODULE__
])
#{:ok, _} = Registry.register(IRC.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
handlers
=
Keyword
.
get
(
Application
.
get_env
(
:lsg
,
__MODULE__
,
[
handlers
:
[]]),
:handlers
)
handler
=
Enum
.
reduce_while
(
handlers
,
nil
,
fn
({
module
,
opts
},
acc
)
->
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
case
module
.
expand
(
uri
,
params
,
opts
)
do
{
:ok
,
data
}
->
{
:ok
,
acc
,
data
}
:error
->
expand_default
(
acc
)
:skip
->
nil
end
rescue
e
->
Logger
.
error
(
inspect
(
e
))
expand_default
(
acc
)
catch
e
,
b
->
Logger
.
error
(
inspect
({
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
(
:lsg
,
__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
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
Fri, Feb 27, 4:08 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
86728
Default Alt Text
link_plugin.ex (8 KB)
Attached To
rNOLA Nola
Event Timeline
Log In to Comment