Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F665768
txt.ex
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
15 KB
Subscribers
None
txt.ex
View Options
defmodule
Nola.Plugins.Txt
do
alias
Nola.UserTrack
require
Logger
@moduledoc
"""
#
[txt]({{context_path}}/txt)
* **.txt**: liste des fichiers et statistiques.
Les fichiers avec une `*` sont vérrouillés.
[Voir sur le web]({{context_path}}/txt).
* **!txt**: lis aléatoirement une ligne dans tous les fichiers.
* **!txt `<recherche>`**: recherche une ligne dans tous les fichiers.
* **~txt**: essaie de générer une phrase (markov).
* **~txt `<début>`**: essaie de générer une phrase commencant par `<debut>`.
* **!`FICHIER`**: lis aléatoirement une ligne du fichier `FICHIER`.
* **!`FICHIER` `<chiffre>`**: lis la ligne `<chiffre>` du fichier `FICHIER`.
* **!`FICHIER` `<recherche>`**: recherche une ligne contenant `<recherche>` dans `FICHIER`.
* **+txt `<file`>**: crée le fichier `<file>`.
* **+`FICHIER` `<texte>`**: ajoute une ligne `<texte>` dans le fichier `FICHIER`.
* **-`FICHIER` `<chiffre>`**: supprime la ligne `<chiffre>` du fichier `FICHIER`.
* **-txtrw, +txtrw**. op seulement. active/désactive le mode lecture seule.
* **+txtlock `<fichier>`, -txtlock `<fichier>`**. op seulement. active/désactive le verrouillage d'un fichier.
Insérez `\\\\` pour faire un saut de ligne.
"""
def
short_irc_doc
,
do
:
"!txt https://sys.115ans.net/irc/txt "
def
irc_doc
,
do
:
@moduledoc
def
start_link
()
do
GenServer
.
start_link
(
__MODULE__
,
[],
name
:
__MODULE__
)
end
defstruct
triggers
:
%{},
rw
:
true
,
locks
:
nil
,
markov_handler
:
nil
,
markov
:
nil
def
random
(
file
)
do
GenServer
.
call
(
__MODULE__
,
{
:random
,
file
})
end
def
reply_random
(
message
,
file
)
do
if
line
=
random
(
file
)
do
line
|>
format_line
(
nil
,
message
)
|>
message
.
replyfun
.
()
line
end
end
def
init
([])
do
dets_locks_filename
=
(
Nola
.
data_path
()
<>
"/"
<>
"txtlocks.dets"
)
|>
String
.
to_charlist
{
:ok
,
locks
}
=
:dets
.
open_file
(
dets_locks_filename
,
[])
markov_handler
=
Keyword
.
get
(
Application
.
get_env
(
:nola
,
__MODULE__
,
[]),
:markov_handler
,
Nola.Plugins.Txt.Markov.Native
)
{
:ok
,
markov
}
=
markov_handler
.
start_link
()
{
:ok
,
_
}
=
Registry
.
register
(
Nola.PubSub
,
"triggers"
,
[
plugin
:
__MODULE__
])
{
:ok
,
%
__MODULE__
{
locks
:
locks
,
markov_handler
:
markov_handler
,
markov
:
markov
,
triggers
:
load
()}}
end
def
handle_info
({
:received
,
"!reload"
,
_
,
chan
},
state
)
do
{
:noreply
,
%
__MODULE__
{
state
|
triggers
:
load
()}}
end
#
# ADMIN: RW/RO
#
def
handle_info
({
:irc
,
:trigger
,
"txtrw"
,
msg
=
%{
channel
:
channel
,
trigger
:
%{
type
:
:plus
}}},
state
=
%{
rw
:
false
})
do
if
channel
&&
UserTrack
.
operator?
(
msg
.
network
,
channel
,
msg
.
sender
.
nick
)
do
msg
.
replyfun
.
(
"txt: écriture réactivée"
)
{
:noreply
,
%
__MODULE__
{
state
|
rw
:
true
}}
else
{
:noreply
,
state
}
end
end
def
handle_info
({
:irc
,
:trigger
,
"txtrw"
,
msg
=
%{
channel
:
channel
,
trigger
:
%{
type
:
:minus
}}},
state
=
%{
rw
:
true
})
do
if
channel
&&
UserTrack
.
operator?
(
msg
.
network
,
channel
,
msg
.
sender
.
nick
)
do
msg
.
replyfun
.
(
"txt: écriture désactivée"
)
{
:noreply
,
%
__MODULE__
{
state
|
rw
:
false
}}
else
{
:noreply
,
state
}
end
end
#
# ADMIN: LOCKS
#
def
handle_info
({
:irc
,
:trigger
,
"txtlock"
,
msg
=
%{
trigger
:
%{
type
:
:plus
,
args
:
[
trigger
]}}},
state
)
do
with
\
{
trigger
,
_
}
<-
clean_trigger
(
trigger
),
true
<-
UserTrack
.
operator?
(
msg
.
network
,
msg
.
channel
,
msg
.
sender
.
nick
)
do
:dets
.
insert
(
state
.
locks
,
{
trigger
})
msg
.
replyfun
.
(
"txt:
#{
trigger
}
verrouillé"
)
end
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"txtlock"
,
msg
=
%{
trigger
:
%{
type
:
:minus
,
args
:
[
trigger
]}}},
state
)
do
with
\
{
trigger
,
_
}
<-
clean_trigger
(
trigger
),
true
<-
UserTrack
.
operator?
(
msg
.
network
,
msg
.
channel
,
msg
.
sender
.
nick
),
true
<-
:dets
.
member
(
state
.
locks
,
trigger
)
do
:dets
.
delete
(
state
.
locks
,
trigger
)
msg
.
replyfun
.
(
"txt:
#{
trigger
}
déverrouillé"
)
end
{
:noreply
,
state
}
end
#
# FILE LIST
#
def
handle_info
({
:irc
,
:trigger
,
"txt"
,
msg
=
%{
trigger
:
%{
type
:
:dot
}}},
state
)
do
map
=
Enum
.
map
(
state
.
triggers
,
fn
({
key
,
data
})
->
ignore?
=
String
.
contains?
(
key
,
"."
)
locked?
=
case
:dets
.
lookup
(
state
.
locks
,
key
)
do
[{
trigger
}]
->
"*"
_
->
""
end
unless
ignore?
,
do
:
"
#{
key
}
:
#{
to_string
(
Enum
.
count
(
data
))
}#{
locked?
}
"
end
)
|>
Enum
.
filter
(
&
&1
)
total
=
Enum
.
reduce
(
state
.
triggers
,
0
,
fn
({
_
,
data
},
acc
)
->
acc
+
Enum
.
count
(
data
)
end
)
detail
=
Enum
.
join
(
map
,
", "
)
total
=
". total:
#{
Enum
.
count
(
state
.
triggers
)
}
fichiers,
#{
to_string
(
total
)
}
lignes. Détail: https://sys.115ans.net/irc/txt"
ro
=
if
!
state
.
rw
,
do
:
" (lecture seule activée)"
,
else
:
""
(
detail
<>
total
<>
ro
)
|>
msg
.
replyfun
.
()
{
:noreply
,
state
}
end
#
# GLOBAL: RANDOM
#
def
handle_info
({
:irc
,
:trigger
,
"txt"
,
msg
=
%{
trigger
:
%{
type
:
:bang
,
args
:
[]}}},
state
)
do
result
=
Enum
.
reduce
(
state
.
triggers
,
[],
fn
({
trigger
,
data
},
acc
)
->
Enum
.
reduce
(
data
,
acc
,
fn
({
l
,
_
},
acc
)
->
[{
trigger
,
l
}
|
acc
]
end
)
end
)
|>
Enum
.
shuffle
()
if
!
Enum
.
empty?
(
result
)
do
{
source
,
line
}
=
Enum
.
random
(
result
)
msg
.
replyfun
.
(
format_line
(
line
,
"
#{
source
}
: "
,
msg
))
end
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"txt"
,
msg
=
%{
trigger
:
%{
type
:
:bang
,
args
:
args
}}},
state
)
do
grep
=
Enum
.
join
(
args
,
" "
)
|>
String
.
downcase
|>
:unicode
.
characters_to_nfd_binary
()
result
=
with_stateful_results
(
msg
,
{
:bang
,
"txt"
,
msg
.
network
,
msg
.
channel
,
grep
},
fn
()
->
Enum
.
reduce
(
state
.
triggers
,
[],
fn
({
trigger
,
data
},
acc
)
->
if
!
String
.
contains?
(
trigger
,
"."
)
do
Enum
.
reduce
(
data
,
acc
,
fn
({
l
,
_
},
acc
)
->
[{
trigger
,
l
}
|
acc
]
end
)
else
acc
end
end
)
|>
Enum
.
filter
(
fn
({
_
,
line
})
->
line
|>
String
.
downcase
()
|>
:unicode
.
characters_to_nfd_binary
()
|>
String
.
contains?
(
grep
)
end
)
|>
Enum
.
shuffle
()
end
)
if
result
do
{
source
,
line
}
=
result
msg
.
replyfun
.
([
"
#{
source
}
: "
|
line
])
end
{
:noreply
,
state
}
end
def
with_stateful_results
(
msg
,
key
,
initfun
)
do
me
=
self
()
scope
=
{
msg
.
network
,
msg
.
channel
||
msg
.
sender
.
nick
}
key
=
{
__MODULE__
,
me
,
scope
,
key
}
with_stateful_results
(
key
,
initfun
)
end
def
with_stateful_results
(
key
,
initfun
)
do
pid
=
case
:global
.
whereis_name
(
key
)
do
:undefined
->
start_stateful_results
(
key
,
initfun
.
())
pid
->
pid
end
if
pid
,
do
:
wait_stateful_results
(
key
,
initfun
,
pid
)
end
def
start_stateful_results
(
key
,
[])
do
nil
end
def
start_stateful_results
(
key
,
list
)
do
me
=
self
()
{
pid
,
_
}
=
spawn_monitor
(
fn
()
->
Process
.
monitor
(
me
)
stateful_results
(
me
,
list
)
end
)
:yes
=
:global
.
register_name
(
key
,
pid
)
pid
end
def
wait_stateful_results
(
key
,
initfun
,
pid
)
do
send
(
pid
,
:get
)
receive
do
{
:stateful_results
,
line
}
->
line
{
:DOWN
,
_ref
,
:process
,
^
pid
,
reason
}
->
with_stateful_results
(
key
,
initfun
)
after
5000
->
nil
end
end
defp
stateful_results
(
owner
,
[])
do
send
(
owner
,
:empty
)
:ok
end
@stateful_results_expire
:timer
.
minutes
(
30
)
defp
stateful_results
(
owner
,
[
line
|
rest
]
=
acc
)
do
receive
do
:get
->
send
(
owner
,
{
:stateful_results
,
line
})
stateful_results
(
owner
,
rest
)
{
:DOWN
,
_ref
,
:process
,
^
owner
,
_
}
->
:ok
after
@stateful_results_expire
->
:ok
end
end
#
# GLOBAL: MARKOV
#
def
handle_info
({
:irc
,
:trigger
,
"txt"
,
msg
=
%{
trigger
:
%{
type
:
:tilde
,
args
:
[]}}},
state
)
do
case
state
.
markov_handler
.
sentence
(
state
.
markov
)
do
{
:ok
,
line
}
->
msg
.
replyfun
.
(
line
)
error
->
Logger
.
error
"Txt Markov error: "
<>
inspect
error
end
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"txt"
,
msg
=
%{
trigger
:
%{
type
:
:tilde
,
args
:
complete
}}},
state
)
do
complete
=
Enum
.
join
(
complete
,
" "
)
case
state
.
markov_handler
.
complete_sentence
(
complete
,
state
.
markov
)
do
{
:ok
,
line
}
->
msg
.
replyfun
.
(
line
)
error
->
Logger
.
error
"Txt Markov error: "
<>
inspect
error
end
{
:noreply
,
state
}
end
#
# TXT CREATE
#
def
handle_info
({
:irc
,
:trigger
,
"txt"
,
msg
=
%{
trigger
:
%{
type
:
:plus
,
args
:
[
trigger
]}}},
state
)
do
with
\
{
trigger
,
_
}
<-
clean_trigger
(
trigger
),
true
<-
can_write?
(
state
,
msg
,
trigger
),
:ok
<-
create_file
(
trigger
)
do
msg
.
replyfun
.
(
"
#{
trigger
}
.txt créé. Ajouter: `+
#{
trigger
}
…` ; Lire: `!
#{
trigger
}
`"
)
{
:noreply
,
%
__MODULE__
{
state
|
triggers
:
load
()}}
else
_
->
{
:noreply
,
state
}
end
end
#
# TXT: RANDOM
#
def
handle_info
({
:irc
,
:trigger
,
trigger
,
m
=
%{
trigger
:
%{
type
:
:query
,
args
:
opts
}}},
state
)
do
{
trigger
,
_
}
=
clean_trigger
(
trigger
)
if
Map
.
get
(
state
.
triggers
,
trigger
)
do
url
=
if
m
.
channel
do
NolaWeb.Router.Helpers
.
irc_url
(
NolaWeb.Endpoint
,
:txt
,
m
.
network
,
NolaWeb
.
format_chan
(
m
.
channel
),
trigger
)
else
NolaWeb.Router.Helpers
.
irc_url
(
NolaWeb.Endpoint
,
:txt
,
trigger
)
end
m
.
replyfun
.
(
"->
#{
url
}
"
)
end
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
trigger
,
msg
=
%{
trigger
:
%{
type
:
:bang
,
args
:
opts
}}},
state
)
do
{
trigger
,
_
}
=
clean_trigger
(
trigger
)
line
=
get_random
(
msg
,
state
.
triggers
,
trigger
,
String
.
trim
(
Enum
.
join
(
opts
,
" "
)))
if
line
do
msg
.
replyfun
.
(
format_line
(
line
,
nil
,
msg
))
end
{
:noreply
,
state
}
end
#
# TXT: ADD
#
def
handle_info
({
:irc
,
:trigger
,
trigger
,
msg
=
%{
trigger
:
%{
type
:
:plus
,
args
:
content
}}},
state
)
do
with
\
true
<-
can_write?
(
state
,
msg
,
trigger
),
{
:ok
,
idx
}
<-
add
(
state
.
triggers
,
msg
.
text
)
do
msg
.
replyfun
.
(
"
#{
msg
.
sender
.
nick
}
: ajouté à
#{
trigger
}
. (
#{
idx
}
)"
)
{
:noreply
,
%
__MODULE__
{
state
|
triggers
:
load
()}}
else
{
:error
,
{
:jaro
,
string
,
idx
}}
->
msg
.
replyfun
.
(
"
#{
msg
.
sender
.
nick
}
: doublon
#{
trigger
}
#
#{
idx
}
:
#{
string
}
"
)
error
->
Logger
.
debug
(
"txt add failed:
#{
inspect
error
}
"
)
{
:noreply
,
state
}
end
end
#
# TXT: DELETE
#
def
handle_info
({
:irc
,
:trigger
,
trigger
,
msg
=
%{
trigger
:
%{
type
:
:minus
,
args
:
[
id
]}}},
state
)
do
with
\
true
<-
can_write?
(
state
,
msg
,
trigger
),
data
<-
Map
.
get
(
state
.
triggers
,
trigger
),
{
id
,
""
}
<-
Integer
.
parse
(
id
),
{
text
,
_id
}
<-
Enum
.
find
(
data
,
fn
({
_
,
idx
})
->
id
-
1
==
idx
end
)
do
data
=
data
|>
Enum
.
into
(
Map
.
new
)
data
=
Map
.
delete
(
data
,
text
)
msg
.
replyfun
.
(
"
#{
msg
.
sender
.
nick
}
:
#{
trigger
}
.txt
#
#{
id
}
supprimée:
#{
text
}
"
)
dump
(
trigger
,
data
)
{
:noreply
,
%
__MODULE__
{
state
|
triggers
:
load
()}}
else
_
->
{
:noreply
,
state
}
end
end
def
handle_info
(
:reload_markov
,
state
=
%
__MODULE__
{
triggers
:
triggers
,
markov
:
markov
})
do
state
.
markov_handler
.
reload
(
state
.
triggers
,
state
.
markov
)
{
:noreply
,
state
}
end
def
handle_info
(
msg
,
state
)
do
{
:noreply
,
state
}
end
def
handle_call
({
:random
,
file
},
_from
,
state
)
do
random
=
get_random
(
nil
,
state
.
triggers
,
file
,
[])
{
:reply
,
random
,
state
}
end
def
terminate
(
_reason
,
state
)
do
if
state
.
locks
do
:dets
.
sync
(
state
.
locks
)
:dets
.
close
(
state
.
locks
)
end
:ok
end
# Load/Reloads text files from disk
defp
load
()
do
triggers
=
Path
.
wildcard
(
directory
()
<>
"/*.txt"
)
|>
Enum
.
reduce
(%{},
fn
(
path
,
m
)
->
file
=
Path
.
basename
(
path
)
key
=
String
.
replace
(
file
,
".txt"
,
""
)
data
=
directory
()
<>
file
|>
File
.
read!
|>
String
.
split
(
"
\n
"
)
|>
Enum
.
reject
(
fn
(
line
)
->
cond
do
line
==
""
->
true
!
line
->
true
true
->
false
end
end
)
|>
Enum
.
with_index
Map
.
put
(
m
,
key
,
data
)
end
)
|>
Enum
.
sort
|>
Enum
.
into
(
Map
.
new
)
send
(
self
(),
:reload_markov
)
triggers
end
defp
dump
(
trigger
,
data
)
do
data
=
data
|>
Enum
.
sort_by
(
fn
({
_
,
idx
})
->
idx
end
)
|>
Enum
.
map
(
fn
({
text
,
_
})
->
text
end
)
|>
Enum
.
join
(
"
\n
"
)
File
.
write!
(
directory
()
<>
"/"
<>
trigger
<>
".txt"
,
data
<>
"
\n
"
,
[])
end
defp
get_random
(
msg
,
triggers
,
trigger
,
[])
do
if
data
=
Map
.
get
(
triggers
,
trigger
)
do
{
data
,
_idx
}
=
Enum
.
random
(
data
)
data
else
nil
end
end
defp
get_random
(
msg
,
triggers
,
trigger
,
opt
)
do
arg
=
case
Integer
.
parse
(
opt
)
do
{
pos
,
""
}
->
{
:index
,
pos
}
{
_pos
,
_some_string
}
->
{
:grep
,
opt
}
_error
->
{
:grep
,
opt
}
end
get_with_param
(
msg
,
triggers
,
trigger
,
arg
)
end
defp
get_with_param
(
msg
,
triggers
,
trigger
,
{
:index
,
pos
})
do
data
=
Map
.
get
(
triggers
,
trigger
,
%{})
case
Enum
.
find
(
data
,
fn
({
_
,
index
})
->
index
+
1
==
pos
end
)
do
{
text
,
_
}
->
text
_
->
nil
end
end
defp
get_with_param
(
msg
,
triggers
,
trigger
,
{
:grep
,
query
})
do
out
=
with_stateful_results
(
msg
,
{
:grep
,
trigger
,
query
},
fn
()
->
data
=
Map
.
get
(
triggers
,
trigger
,
%{})
regex
=
Regex
.
compile!
(
"
#{
query
}
"
,
"i"
)
Enum
.
filter
(
data
,
fn
({
txt
,
_
})
->
Regex
.
match?
(
regex
,
txt
)
end
)
|>
Enum
.
map
(
fn
({
txt
,
_
})
->
txt
end
)
|>
Enum
.
shuffle
()
end
)
if
out
,
do
:
out
end
defp
create_file
(
name
)
do
File
.
touch!
(
directory
()
<>
"/"
<>
name
<>
".txt"
)
:ok
end
defp
add
(
triggers
,
trigger_and_content
)
do
case
String
.
split
(
trigger_and_content
,
" "
,
parts
:
2
)
do
[
trigger
,
content
]
->
{
trigger
,
_
}
=
clean_trigger
(
trigger
)
if
Map
.
has_key?
(
triggers
,
trigger
)
do
jaro
=
Enum
.
find
(
triggers
[
trigger
],
fn
({
string
,
idx
})
->
String
.
jaro_distance
(
content
,
string
)
>
0.9
end
)
if
jaro
do
{
string
,
idx
}
=
jaro
{
:error
,
{
:jaro
,
string
,
idx
}}
else
File
.
write!
(
directory
()
<>
"/"
<>
trigger
<>
".txt"
,
content
<>
"
\n
"
,
[
:append
])
idx
=
Enum
.
count
(
triggers
[
trigger
])
+
1
{
:ok
,
idx
}
end
else
{
:error
,
:notxt
}
end
_
->
{
:error
,
:badarg
}
end
end
# fixme: this is definitely the ugliest thing i've ever done
defp
clean_trigger
(
trigger
)
do
[
trigger
|
opts
]
=
trigger
|>
String
.
strip
|>
String
.
split
(
" "
,
parts
:
2
)
trigger
=
trigger
|>
String
.
downcase
|>
:unicode
.
characters_to_nfd_binary
()
|>
String
.
replace
(
~r/[^a-z0-9._]/
,
""
)
|>
String
.
trim
(
"."
)
|>
String
.
trim
(
"_"
)
{
trigger
,
opts
}
end
def
format_line
(
line
,
prefix
,
msg
)
do
prefix
=
unless
(
prefix
,
do
:
""
,
else
:
prefix
)
prefix
<>
line
|>
String
.
split
(
"\\\\"
)
|>
Enum
.
map
(
fn
(
line
)
->
String
.
split
(
line
,
"\\\\\\\\"
)
end
)
|>
List
.
flatten
()
|>
Enum
.
map
(
fn
(
line
)
->
String
.
trim
(
line
)
|>
Tmpl
.
render
(
msg
)
end
)
end
def
directory
()
do
Application
.
get_env
(
:nola
,
:data_path
)
<>
"/irc.txt/"
end
defp
can_write?
(%{
rw
:
rw?
,
locks
:
locks
},
msg
=
%{
channel
:
nil
,
sender
:
sender
},
trigger
)
do
admin?
=
IRC
.
admin?
(
sender
)
locked?
=
case
:dets
.
lookup
(
locks
,
trigger
)
do
[{
trigger
}]
->
true
_
->
false
end
unlocked?
=
if
rw?
==
false
,
do
:
false
,
else
:
!
locked?
can?
=
unlocked?
||
admin?
if
!
can?
do
reason
=
if
!
rw?
,
do
:
"lecture seule"
,
else
:
"fichier vérrouillé"
msg
.
replyfun
.
(
"
#{
sender
.
nick
}
: permission refusée (
#{
reason
}
)"
)
end
can?
end
defp
can_write?
(
state
=
%
__MODULE__
{
rw
:
rw?
,
locks
:
locks
},
msg
=
%{
channel
:
channel
,
sender
:
sender
},
trigger
)
do
admin?
=
IRC
.
admin?
(
sender
)
operator?
=
Nola.UserTrack
.
operator?
(
msg
.
network
,
channel
,
sender
.
nick
)
locked?
=
case
:dets
.
lookup
(
locks
,
trigger
)
do
[{
trigger
}]
->
true
_
->
false
end
unlocked?
=
if
rw?
==
false
,
do
:
false
,
else
:
!
locked?
can?
=
admin?
||
operator?
||
unlocked?
if
!
can?
do
reason
=
if
!
rw?
,
do
:
"lecture seule"
,
else
:
"fichier vérrouillé"
msg
.
replyfun
.
(
"
#{
sender
.
nick
}
: permission refusée (
#{
reason
}
)"
)
end
can?
end
end
File Metadata
Details
Attached
Mime Type
text/x-ruby
Expires
Sat, Feb 28, 7:15 AM (8 h, 9 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
87411
Default Alt Text
txt.ex (15 KB)
Attached To
rNOLA Nola
Event Timeline
Log In to Comment