Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F77364
alcoolog.ex
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
46 KB
Subscribers
None
alcoolog.ex
View Options
defmodule
Nola.Plugins.Alcoolog
do
require
Logger
@moduledoc
"""
#
[alcoolog]({{context_path}}/alcoolog)
* **!santai `<cl | (calc)>` `<degrés d'alcool> [annotation]`**: enregistre un nouveau verre de `montant` d'une boisson à `degrés d'alcool`.
* **!santai `<cl | (calc)>` `<beer name>`**: enregistre un nouveau verre de `cl` de la bière `beer name`, et checkin sur Untappd.com.
* **!moar `[cl]` : enregistre un verre équivalent au dernier !santai.
* **-santai**: annule la dernière entrée d'alcoolisme.
* **.alcoolisme**: état du channel en temps réel.
* **.alcoolisme `<semaine | Xj>`**: points par jour, sur X j.
* **!alcoolisme `[pseudo]`**: affiche les points d'alcoolisme.
* **!alcoolisme `[pseudo]` `<semaine | Xj>`**: affiche les points d'alcoolisme par jour sur X j.
* **+alcoolisme `<h|f>` `<poids en kg>` `[facteur de perte en mg/l (10, 15, 20, 25)]`**: Configure votre profil d'alcoolisme.
* **.sobre**: affiche quand la sobriété frappera sur le chan.
* **!sobre `[pseudo]`**: affiche quand la sobriété frappera pour `[pseudo]`.
* **!sobrepour `<date>`**: affiche tu pourras être sobre pour `<date>`, et si oui, combien de volumes d'alcool peuvent encore être consommés.
* **!alcoolog**: ([voir]({{context_path}}/alcoolog)) lien pour voir l'état/statistiques et historique de l'alcoolémie du channel.
* **!alcool `<cl>` `<degrés>`**: donne le nombre d'unités d'alcool dans `<cl>` à `<degrés>°`.
* **!soif**: c'est quand l'apéro ?
1 point = 1 volume d'alcool.
Annotation: champ libre!
---
##
`!txt`s
* status utilisateur: `alcoolog.user_(sober|legal|legalhigh|high|toohigh|sick)(|_rising)`
* mauvaises boissons: `alcoolog.drink_(negative|zero|negative)`
* santo: `alcoolog.santo`
* santai: `alcoolog.santai`
* plus gros, moins gros: `alcoolog.(fatter|thinner)`
"""
def
irc_doc
,
do
:
@moduledoc
def
start_link
(),
do
:
GenServer
.
start_link
(
__MODULE__
,
[],
name
:
__MODULE__
)
# tuple dets: {nick, date, volumes, current_alcohol_level, nom, commentaire}
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
# tuple meta dets: {nick, map}
# %{:weight => float, :sex => true(h),false(f)}
@pubsub
~w(account)
@pubsub_triggers
~w(santai moar again bis santo santeau alcoolog sobre sobrepour soif alcoolisme alcool)
@default_user_meta
%{
weight
:
77.4
,
sex
:
true
,
loss_factor
:
15
}
def
data_state
()
do
dets_filename
=
(
Nola
.
data_path
()
<>
"/"
<>
"alcoolisme.dets"
)
|>
String
.
to_charlist
dets_meta_filename
=
(
Nola
.
data_path
()
<>
"/"
<>
"alcoolisme_meta.dets"
)
|>
String
.
to_charlist
%{
dets
:
dets_filename
,
meta
:
dets_meta_filename
,
ets
:
__MODULE__
.
ETS
}
end
def
init
(
_
)
do
triggers
=
for
(
t
<-
@pubsub_triggers
,
do
:
"trigger:"
<>
t
)
for
sub
<-
@pubsub
++
triggers
do
{
:ok
,
_
}
=
Registry
.
register
(
Nola.PubSub
,
sub
,
plugin
:
__MODULE__
)
end
dets_filename
=
(
Nola
.
data_path
()
<>
"/"
<>
"alcoolisme.dets"
)
|>
String
.
to_charlist
{
:ok
,
dets
}
=
:dets
.
open_file
(
dets_filename
,
[{
:type
,
:bag
}])
ets
=
:ets
.
new
(
__MODULE__
.
ETS
,
[
:ordered_set
,
:named_table
,
:protected
,
{
:read_concurrency
,
true
}])
dets_meta_filename
=
(
Nola
.
data_path
()
<>
"/"
<>
"alcoolisme_meta.dets"
)
|>
String
.
to_charlist
{
:ok
,
meta
}
=
:dets
.
open_file
(
dets_meta_filename
,
[{
:type
,
:set
}])
traverse_fun
=
fn
(
obj
,
dets
)
->
case
obj
do
object
=
{
nick
,
naive
=
%
NaiveDateTime
{},
volumes
,
active
,
cl
,
deg
,
name
,
comment
}
->
date
=
naive
|>
DateTime
.
from_naive!
(
"Etc/UTC"
)
|>
DateTime
.
to_unix
()
new
=
{
nick
,
date
,
volumes
,
active
,
cl
,
deg
,
name
,
comment
,
Map
.
new
()}
:dets
.
delete_object
(
dets
,
object
)
:dets
.
insert
(
dets
,
new
)
:ets
.
insert
(
ets
,
{{
nick
,
date
},
volumes
,
active
,
cl
,
deg
,
name
,
comment
,
Map
.
new
()})
dets
object
=
{
nick
,
naive
=
%
NaiveDateTime
{},
volumes
,
active
,
cl
,
deg
,
name
,
comment
,
meta
}
->
date
=
naive
|>
DateTime
.
from_naive!
(
"Etc/UTC"
)
|>
DateTime
.
to_unix
()
new
=
{
nick
,
date
,
volumes
,
active
,
cl
,
deg
,
name
,
comment
,
Map
.
new
()}
:dets
.
delete_object
(
dets
,
object
)
:dets
.
insert
(
dets
,
new
)
:ets
.
insert
(
ets
,
{{
nick
,
date
},
volumes
,
active
,
cl
,
deg
,
name
,
comment
,
Map
.
new
()})
dets
object
=
{
nick
,
date
,
volumes
,
active
,
cl
,
deg
,
name
,
comment
,
meta
}
->
:ets
.
insert
(
ets
,
{{
nick
,
date
},
volumes
,
active
,
cl
,
deg
,
name
,
comment
,
meta
})
dets
_
->
dets
end
end
:dets
.
foldl
(
traverse_fun
,
dets
,
dets
)
:dets
.
sync
(
dets
)
state
=
%{
dets
:
dets
,
meta
:
meta
,
ets
:
ets
}
{
:ok
,
state
}
end
@eau
[
"santo"
,
"santeau"
]
def
handle_info
({
:irc
,
:trigger
,
santeau
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
_
,
type
:
:bang
}}},
state
)
when
santeau
in
@eau
do
Nola.Plugins.Txt
.
reply_random
(
m
,
"alcoolog.santo"
)
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"soif"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
_
,
type
:
:bang
}}},
state
)
do
now
=
DateTime
.
utc_now
()
|>
Timex.Timezone
.
convert
(
"Europe/Paris"
)
apero
=
format_duration_from_now
(%
DateTime
{
now
|
hour
:
18
,
minute
:
0
,
second
:
0
},
false
)
day_of_week
=
Date
.
day_of_week
(
now
)
{
txt
,
apero?
}
=
cond
do
now
.
hour
>=
0
&&
now
.
hour
<
6
->
{[
"apéro tardif ? Je dis OUI ! SANTAI !"
],
true
}
now
.
hour
>=
6
&&
now
.
hour
<
12
->
if
day_of_week
>=
6
do
{[
"de l'alcool pour le petit dej ? le week-end, pas de problème !"
],
true
}
else
{[
"C'est quand même un peu tôt non ? Prochain apéro
#{
apero
}
"
],
false
}
end
now
.
hour
>=
12
&&
(
now
.
hour
<
14
)
->
{[
"oui! c'est l'apéro de midi! (et apéro
#{
apero
}
)"
,
"tu peux attendre
#{
apero
}
ou y aller, il est midi !"
],
true
}
now
.
hour
==
17
->
{[
"ÇA APPROCHE !!! Apéro
#{
apero
}
"
,
"BIENTÔT !!! Apéro
#{
apero
}
"
,
"achetez vite les teilles, apéro dans
#{
apero
}
!"
,
"préparez les teilles, apéro dans
#{
apero
}
!"
],
false
}
now
.
hour
>=
14
&&
now
.
hour
<
18
->
weekend
=
if
day_of_week
>=
6
do
" ... ou maintenant en fait, c'est le week-end!"
else
""
end
{[
"tiens bon! apéro
#{
apero
}#{
weekend
}
"
,
"courage... apéro dans
#{
apero
}#{
weekend
}
"
,
"pas encore :'( apéro dans
#{
apero
}#{
weekend
}
"
],
false
}
true
->
{[
"C'EST L'HEURE DE L'APÉRO !!! SANTAIIIIIIIIIIII !!!!"
],
true
}
end
txt
=
txt
|>
Enum
.
shuffle
()
|>
Enum
.
random
()
m
.
replyfun
.
(
txt
)
stats
=
get_full_statistics
(
state
,
m
.
account
.
id
)
if
!
apero?
&&
stats
.
active
>
0.1
do
m
.
replyfun
.
(
"(... ou continue en fait, je suis pas ta mère !)"
)
end
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"sobrepour"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
args
,
type
:
:bang
}}},
state
)
do
args
=
Enum
.
join
(
args
,
" "
)
{
:ok
,
now
}
=
DateTime
.
now
(
"Europe/Paris"
,
Tzdata.TimeZoneDatabase
)
time
=
case
args
do
"demain "
<>
time
->
{
h
,
m
}
=
case
String
.
split
(
time
,
[
":"
,
"h"
])
do
[
hour
,
""
]
->
IO
.
puts
(
"h
#{
inspect
hour
}
"
)
{
h
,
_
}
=
Integer
.
parse
(
hour
)
{
h
,
0
}
[
hour
,
min
]
when
min
!=
""
->
{
h
,
_
}
=
Integer
.
parse
(
hour
)
{
m
,
_
}
=
Integer
.
parse
(
min
)
{
h
,
m
}
[
hour
]
->
IO
.
puts
(
"h
#{
inspect
hour
}
"
)
{
h
,
_
}
=
Integer
.
parse
(
hour
)
{
h
,
0
}
_
->
{
0
,
0
}
end
secs
=
((
60
*
60
)
*
24
)
day
=
DateTime
.
add
(
now
,
secs
,
:second
,
Tzdata.TimeZoneDatabase
)
%
DateTime
{
day
|
hour
:
h
,
minute
:
m
,
second
:
0
}
"après demain "
<>
time
->
secs
=
2
*
((
60
*
60
)
*
24
)
DateTime
.
add
(
now
,
secs
,
:second
,
Tzdata.TimeZoneDatabase
)
datetime
->
case
Timex.Parse.DateTime.Parser
.
parse
(
datetime
,
"{}"
)
do
{
:ok
,
dt
}
->
dt
_
->
nil
end
end
if
time
do
meta
=
get_user_meta
(
state
,
m
.
account
.
id
)
stats
=
get_full_statistics
(
state
,
m
.
account
.
id
)
duration
=
round
(
DateTime
.
diff
(
time
,
now
)
/
60.0
)
IO
.
puts
"diff
#{
inspect
duration
}
sober in
#{
inspect
stats
.
sober_in
}
"
if
duration
<
stats
.
sober_in
do
int
=
stats
.
sober_in
-
duration
m
.
replyfun
.
(
"désolé, aucune chance! tu seras sobre
#{
format_minute_duration
(
int
)
}
après!"
)
else
remaining
=
duration
-
stats
.
sober_in
if
remaining
<
30
do
m
.
replyfun
.
(
"moins de 30 minutes de sobriété, c'est impossible de boire plus"
)
else
loss_per_minute
=
((
meta
.
loss_factor
/
100
)
/
60
)
remaining_gl
=
(
remaining
-
30
)
*
loss_per_minute
m
.
replyfun
.
(
"marge de boisson:
#{
inspect
remaining
}
minutes,
#{
remaining_gl
}
g/l"
)
end
end
end
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"alcoolog"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
[],
type
:
:plus
}}},
state
)
do
{
:ok
,
token
}
=
Nola.Token
.
new
({
:alcoolog
,
:index
,
m
.
sender
.
network
,
m
.
channel
})
url
=
NolaWeb.Router.Helpers
.
alcoolog_url
(
NolaWeb.Endpoint
,
:index
,
m
.
network
,
NolaWeb
.
format_chan
(
m
.
channel
),
token
)
m
.
replyfun
.
(
"->
#{
url
}
"
)
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"alcoolog"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
[],
type
:
:bang
}}},
state
)
do
url
=
NolaWeb.Router.Helpers
.
alcoolog_url
(
NolaWeb.Endpoint
,
:index
,
m
.
network
,
NolaWeb
.
format_chan
(
m
.
channel
))
m
.
replyfun
.
(
"->
#{
url
}
"
)
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"alcool"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
args
=
[
cl
,
deg
],
type
:
:bang
}}},
state
)
do
{
cl
,
_
}
=
Util
.
float_paparse
(
cl
)
{
deg
,
_
}
=
Util
.
float_paparse
(
deg
)
points
=
Alcool
.
units
(
cl
,
deg
)
meta
=
get_user_meta
(
state
,
m
.
account
.
id
)
k
=
if
meta
.
sex
,
do
:
0.7
,
else
:
0.6
weight
=
meta
.
weight
gl
=
(
10
*
points
)
/
(
k
*
weight
)
duration
=
round
(
gl
/
((
meta
.
loss_factor
/
100
)
/
60
))
+
30
sober_in_s
=
if
duration
>
0
do
duration
=
Timex.Duration
.
from_minutes
(
duration
)
Timex.Format.Duration.Formatter
.
lformat
(
duration
,
"fr"
,
:humanized
)
else
""
end
m
.
replyfun
.
(
"Il y a
#{
Float
.
round
(
points
+
0.0
,
4
)
}
unités d'alcool dans
#{
cl
}
cl à
#{
deg
}
° (
#{
Float
.
round
(
gl
+
0.0
,
4
)
}
g/l,
#{
sober_in_s
}
)"
)
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"santai"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
[
cl
,
deg
|
comment
],
type
:
:bang
}}},
state
)
do
santai
(
m
,
state
,
cl
,
deg
,
comment
)
{
:noreply
,
state
}
end
@moar
[
"{{message.sender.nick}}: la même donc ?"
,
"{{message.sender.nick}}: et voilà la petite sœur !"
]
def
handle_info
({
:irc
,
:trigger
,
"bis"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
args
,
type
:
:bang
}}},
state
)
do
handle_info
({
:irc
,
:trigger
,
"moar"
,
m
},
state
)
end
def
handle_info
({
:irc
,
:trigger
,
"again"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
args
,
type
:
:bang
}}},
state
)
do
handle_info
({
:irc
,
:trigger
,
"moar"
,
m
},
state
)
end
def
handle_info
({
:irc
,
:trigger
,
"moar"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
args
,
type
:
:bang
}}},
state
)
do
case
get_statistics_for_nick
(
state
,
m
.
account
.
id
)
do
{
_
,
obj
=
{
_
,
_date
,
_points
,
_active
,
cl
,
deg
,
_name
,
comment
,
_meta
}}
->
cl
=
case
args
do
[
cls
]
->
case
Util
.
float_paparse
(
cls
)
do
{
cl
,
_
}
->
cl
_
->
cl
end
_
->
cl
end
moar
=
@moar
|>
Enum
.
shuffle
()
|>
Enum
.
random
()
|>
Tmpl
.
render
(
m
)
|>
m
.
replyfun
.
()
santai
(
m
,
state
,
cl
,
deg
,
comment
,
auto_set
:
true
)
{
_
,
obj
=
{
_
,
date
,
points
,
_last_active
,
type
,
descr
}}
->
case
Regex
.
named_captures
(
~r/^(?<cl>
\d
+[.]
\d
+)cl
\s
+(?<deg>
\d
+[.]
\d
+)°$/
,
type
)
do
nil
->
m
.
replyfun
.
(
"suce"
)
u
->
moar
=
@moar
|>
Enum
.
shuffle
()
|>
Enum
.
random
()
|>
Tmpl
.
render
(
m
)
|>
m
.
replyfun
.
()
santai
(
m
,
state
,
u
[
"cl"
],
u
[
"deg"
],
descr
,
auto_set
:
true
)
end
_
->
nil
end
{
:noreply
,
state
}
end
defp
santai
(
m
,
state
,
cl
,
deg
,
comment
,
options
\\
[])
do
comment
=
cond
do
comment
==
[]
->
nil
is_binary
(
comment
)
->
comment
comment
==
nil
->
nil
true
->
Enum
.
join
(
comment
,
" "
)
end
{
cl
,
cl_extra
}
=
case
{
Util
.
float_paparse
(
cl
),
cl
}
do
{{
cl
,
extra
},
_
}
->
{
cl
,
extra
}
{
:error
,
"("
<>
_
}
->
try
do
{
:ok
,
result
}
=
Abacus
.
eval
(
cl
)
{
result
,
nil
}
rescue
_
->
{
nil
,
"cl: invalid calc expression"
}
end
{
:error
,
_
}
->
{
nil
,
"cl: invalid value"
}
end
{
deg
,
comment
,
auto_set
,
beer_id
}
=
case
Util
.
float_paparse
(
deg
)
do
{
deg
,
_
}
->
{
deg
,
comment
,
Keyword
.
get
(
options
,
:auto_set
,
false
),
nil
}
:error
->
beername
=
if
(
comment
,
do
:
"
#{
deg
}
#{
comment
}
"
,
else
:
deg
)
case
Untappd
.
search_beer
(
beername
,
limit
:
1
)
do
{
:ok
,
%{
"response"
=>
%{
"beers"
=>
%{
"count"
=>
count
,
"items"
=>
[%{
"beer"
=>
beer
,
"brewery"
=>
brewery
}
|
_
]}}}}
->
{
Map
.
get
(
beer
,
"beer_abv"
),
"
#{
Map
.
get
(
brewery
,
"brewery_name"
)
}
:
#{
Map
.
get
(
beer
,
"beer_name"
)
}
"
,
true
,
Map
.
get
(
beer
,
"bid"
)}
_
->
{
deg
,
"could not find beer"
,
false
,
nil
}
end
end
cond
do
cl
==
nil
->
m
.
replyfun
.
(
cl_extra
)
deg
==
nil
->
m
.
replyfun
.
(
comment
)
cl
>=
500
||
deg
>=
100
->
Nola.Plugins.Txt
.
reply_random
(
m
,
"alcoolog.drink_toohuge"
)
cl
==
0
||
deg
==
0
->
Nola.Plugins.Txt
.
reply_random
(
m
,
"alcoolog.drink_zero"
)
cl
<
0
||
deg
<
0
->
Nola.Plugins.Txt
.
reply_random
(
m
,
"alcoolog.drink_negative"
)
true
->
points
=
Alcool
.
units
(
cl
,
deg
)
now
=
m
.
at
||
DateTime
.
utc_now
()
|>
DateTime
.
to_unix
(
:millisecond
)
user_meta
=
get_user_meta
(
state
,
m
.
account
.
id
)
name
=
"
#{
cl
}
cl
#{
deg
}
°"
old_stats
=
get_full_statistics
(
state
,
m
.
account
.
id
)
meta
=
%{}
meta
=
Map
.
put
(
meta
,
"timestamp"
,
now
)
meta
=
Map
.
put
(
meta
,
"weight"
,
user_meta
.
weight
)
meta
=
Map
.
put
(
meta
,
"sex"
,
user_meta
.
sex
)
:ok
=
:dets
.
insert
(
state
.
dets
,
{
m
.
account
.
id
,
now
,
points
,
if
(
old_stats
,
do
:
old_stats
.
active
,
else
:
0
),
cl
,
deg
,
name
,
comment
,
meta
})
true
=
:ets
.
insert
(
state
.
ets
,
{{
m
.
account
.
id
,
now
},
points
,
if
(
old_stats
,
do
:
old_stats
.
active
,
else
:
0
),
cl
,
deg
,
name
,
comment
,
meta
})
#sante = @santai |> Enum.map(fn(s) -> String.trim(String.upcase(s)) end) |> Enum.shuffle() |> Enum.random()
sante
=
Nola.Plugins.Txt
.
random
(
"alcoolog.santai"
)
k
=
if
user_meta
.
sex
,
do
:
0.7
,
else
:
0.6
weight
=
user_meta
.
weight
peak
=
Float
.
round
((
10
*
points
||
0.0
)
/
(
k
*
weight
),
4
)
stats
=
get_full_statistics
(
state
,
m
.
account
.
id
)
sober_add
=
if
old_stats
&&
Map
.
get
(
old_stats
||
%{},
:sober_in
)
do
mins
=
round
(
stats
.
sober_in
-
old_stats
.
sober_in
)
" [+
#{
mins
}
m]"
else
""
end
nonow
=
DateTime
.
utc_now
()
sober
=
nonow
|>
DateTime
.
add
(
round
(
stats
.
sober_in
*
60
),
:second
)
|>
Timex.Timezone
.
convert
(
"Europe/Paris"
)
at
=
if
nonow
.
day
==
sober
.
day
do
{
:ok
,
detail
}
=
Timex.Format.DateTime.Formatters.Default
.
lformat
(
sober
,
"aujourd'hui {h24}:{m}"
,
"fr"
)
detail
else
{
:ok
,
detail
}
=
Timex.Format.DateTime.Formatters.Default
.
lformat
(
sober
,
"{WDfull} {h24}:{m}"
,
"fr"
)
detail
end
up
=
if
stats
.
active_drinks
>
1
do
" "
<>
Enum
.
join
(
for
(
_
<-
1
..
stats
.
active_drinks
,
do
:
"▲"
))
<>
""
else
""
end
since_str
=
if
stats
.
since
&&
stats
.
since_min
>
180
do
"(depuis:
#{
stats
.
since_s
}
) "
else
""
end
msg
=
fn
(
nick
,
extra
)
->
"
#{
sante
}
#{
nick
}
#{
extra
}#{
up
}
#{
format_points
(
points
)
}
@
#{
stats
.
active
}
g/l [+
#{
peak
}
g/l]"
<>
" (15m:
#{
stats
.
active15m
}
, 30m:
#{
stats
.
active30m
}
, 1h:
#{
stats
.
active1h
}
)
#{
since_str
}
(sobriété
#{
at
}
(dans
#{
stats
.
sober_in_s
}
)
#{
sober_add
}
) !"
<>
" (aujourd'hui
#{
stats
.
daily_volumes
}
points -
#{
stats
.
daily_gl
}
g/l)"
end
meta
=
if
beer_id
do
Map
.
put
(
meta
,
"untappd:beer_id"
,
beer_id
)
else
meta
end
if
beer_id
do
spawn
(
fn
()
->
case
Untappd
.
maybe_checkin
(
m
.
account
,
beer_id
)
do
{
:ok
,
body
}
->
badges
=
get_in
(
body
,
[
"badges"
,
"items"
])
if
badges
!=
[]
do
badges_s
=
Enum
.
map
(
badges
,
fn
(
badge
)
->
Map
.
get
(
badge
,
"badge_name"
)
end
)
|>
Enum
.
filter
(
fn
(
b
)
->
b
end
)
|>
Enum
.
intersperse
(
", "
)
|>
Enum
.
join
(
""
)
badge
=
if
(
length
(
badges
)
>
1
,
do
:
"badges"
,
else
:
"badge"
)
m
.
replyfun
.
(
"\\O/ Unlocked untappd
#{
badge
}
:
#{
badges_s
}
"
)
end
:ok
{
:error
,
{
:http_error
,
error
}}
when
is_integer
(
error
)
->
m
.
replyfun
.
(
"Checkin to Untappd failed:
#{
to_string
(
error
)
}
"
)
{
:error
,
{
:http_error
,
error
}}
->
m
.
replyfun
.
(
"Checkin to Untappd failed:
#{
inspect
error
}
"
)
_
->
:error
end
end
)
end
local_extra
=
if
auto_set
do
if
comment
do
"
#{
comment
}
(
#{
cl
}
cl @
#{
deg
}
°)"
else
"
#{
cl
}
cl @
#{
deg
}
°"
end
else
""
end
m
.
replyfun
.
(
msg
.
(
m
.
sender
.
nick
,
local_extra
))
notify
=
Nola.Membership
.
notify_channels
(
m
.
account
)
--
[{
m
.
network
,
m
.
channel
}]
for
{
net
,
chan
}
<-
notify
do
user
=
Nola.UserTrack
.
find_by_account
(
net
,
m
.
account
)
nick
=
if
(
user
,
do
:
user
.
nick
,
else
:
m
.
account
.
name
)
extra
=
" "
<>
present_type
(
name
,
comment
)
<>
""
Nola.Irc.Connection
.
broadcast_message
(
net
,
chan
,
msg
.
(
nick
,
extra
))
end
miss
=
cond
do
points
<=
0.6
->
:small
stats
.
active30m
>=
2.9
&&
stats
.
active30m
<
3
->
:miss3
stats
.
active30m
>=
1.9
&&
stats
.
active30m
<
2
->
:miss2
stats
.
active30m
>=
0.9
&&
stats
.
active30m
<
1
->
:miss1
stats
.
active30m
>=
0.45
&&
stats
.
active30m
<
0.5
->
:miss05
stats
.
active30m
>=
0.20
&&
stats
.
active30m
<
0.20
->
:miss025
stats
.
active30m
>=
3
&&
stats
.
active1h
<
3.15
->
:small3
stats
.
active30m
>=
2
&&
stats
.
active1h
<
2.15
->
:small2
stats
.
active30m
>=
1.5
&&
stats
.
active1h
<
1.5
->
:small15
stats
.
active30m
>=
1
&&
stats
.
active1h
<
1.15
->
:small1
stats
.
active30m
>=
0.5
&&
stats
.
active1h
<=
0.51
->
:small05
stats
.
active30m
>=
0.25
&&
stats
.
active30m
<=
0.255
->
:small025
true
->
nil
end
if
miss
do
miss
=
Nola.Plugins.Txt
.
random
(
"alcoolog.
#{
to_string
(
miss
)
}
"
)
if
miss
do
for
{
net
,
chan
}
<-
Nola.Membership
.
notify_channels
(
m
.
account
)
do
user
=
Nola.UserTrack
.
find_by_account
(
net
,
m
.
account
)
nick
=
if
(
user
,
do
:
user
.
nick
,
else
:
m
.
account
.
name
)
Nola.Irc.Connection
.
broadcast_message
(
net
,
chan
,
"
#{
nick
}
:
#{
miss
}
"
)
end
end
end
end
end
def
handle_info
({
:irc
,
:trigger
,
"santai"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
_
,
type
:
:bang
}}},
state
)
do
m
.
replyfun
.
(
"!santai <cl> <degrés> [commentaire]"
)
{
:noreply
,
state
}
end
def
get_all_stats
()
do
Nola.Account
.
all_accounts
()
|>
Enum
.
map
(
fn
(
account
)
->
{
account
.
id
,
get_full_statistics
(
account
.
id
)}
end
)
|>
Enum
.
filter
(
fn
({
_nick
,
status
})
->
status
&&
(
status
.
active
>
0
||
status
.
active30m
>
0
)
end
)
|>
Enum
.
sort_by
(
fn
({
_
,
status
})
->
status
.
active
end
,
&>/
2
)
end
def
get_channel_statistics
(
account
,
network
,
nil
)
do
Nola.Membership
.
expanded_members_or_friends
(
account
,
network
,
nil
)
|>
Enum
.
map
(
fn
({
account
,
_
,
nick
})
->
{
nick
,
get_full_statistics
(
account
.
id
)}
end
)
|>
Enum
.
filter
(
fn
({
_nick
,
status
})
->
status
&&
(
status
.
active
>
0
||
status
.
active30m
>
0
)
end
)
|>
Enum
.
sort_by
(
fn
({
_
,
status
})
->
status
.
active
end
,
&>/
2
)
end
def
get_channel_statistics
(
_
,
network
,
channel
),
do
:
get_channel_statistics
(
network
,
channel
)
def
get_channel_statistics
(
network
,
channel
)
do
Nola.Membership
.
expanded_members
(
network
,
channel
)
|>
Enum
.
map
(
fn
({
account
,
_
,
nick
})
->
{
nick
,
get_full_statistics
(
account
.
id
)}
end
)
|>
Enum
.
filter
(
fn
({
_nick
,
status
})
->
status
&&
(
status
.
active
>
0
||
status
.
active30m
>
0
)
end
)
|>
Enum
.
sort_by
(
fn
({
_
,
status
})
->
status
.
active
end
,
&>/
2
)
end
@spec
since
()
::
%{
Nola.Account
.
id
()
=>
DateTime
.
t
()}
@doc
"Returns the last time the user was at 0 g/l"
def
since
()
do
:ets
.
foldr
(
fn
({{
acct
,
timestamp_or_date
},
_vol
,
current
,
_cl
,
_deg
,
_name
,
_comment
,
_m
},
acc
)
->
if
!
Map
.
get
(
acc
,
acct
)
&&
current
==
0
do
date
=
Util
.
to_date_time
(
timestamp_or_date
)
Map
.
put
(
acc
,
acct
,
date
)
else
acc
end
end
,
%{},
__MODULE__
.
ETS
)
end
def
get_full_statistics
(
nick
)
do
get_full_statistics
(
data_state
(),
nick
)
end
defp
get_full_statistics
(
state
,
nick
)
do
case
get_statistics_for_nick
(
state
,
nick
)
do
{
count
,
{
_
,
last_at
,
last_points
,
last_active
,
last_cl
,
last_deg
,
last_type
,
last_descr
,
_meta
}}
->
{
active
,
active_drinks
}
=
current_alcohol_level
(
state
,
nick
)
{
_
,
m30
}
=
alcohol_level_rising
(
state
,
nick
)
{
rising
,
m15
}
=
alcohol_level_rising
(
state
,
nick
,
15
)
{
_
,
m5
}
=
alcohol_level_rising
(
state
,
nick
,
5
)
{
_
,
h1
}
=
alcohol_level_rising
(
state
,
nick
,
60
)
trend
=
if
rising
do
"▲"
else
"▼"
end
user_state
=
cond
do
active
<=
0.0
->
:sober
active
<=
0.25
->
:low
active
<=
0.50
->
:legal
active
<=
1.0
->
:legalhigh
active
<=
2.5
->
:high
active
<
3
->
:toohigh
true
->
:sick
end
rising_file_key
=
if
rising
,
do
:
"_rising"
,
else
:
""
txt_file
=
"alcoolog."
<>
"user_"
<>
to_string
(
user_state
)
<>
rising_file_key
user_status
=
Nola.Plugins.Txt
.
random
(
txt_file
)
meta
=
get_user_meta
(
state
,
nick
)
minutes_til_sober
=
h1
/
((
meta
.
loss_factor
/
100
)
/
60
)
minutes_til_sober
=
cond
do
active
<
0
->
0
m15
<
0
->
15
m30
<
0
->
30
h1
<
0
->
60
minutes_til_sober
>
0
->
Float
.
round
(
minutes_til_sober
+
60
)
true
->
0
end
duration
=
Timex.Duration
.
from_minutes
(
minutes_til_sober
)
sober_in_s
=
if
minutes_til_sober
>
0
do
Timex.Format.Duration.Formatter
.
lformat
(
duration
,
"fr"
,
:humanized
)
else
nil
end
since
=
if
active
>
0
do
since
()
|>
Map
.
get
(
nick
)
end
since_diff
=
if
since
,
do
:
Timex
.
diff
(
DateTime
.
utc_now
(),
since
,
:minutes
)
since_duration
=
if
since
,
do
:
Timex.Duration
.
from_minutes
(
since_diff
)
since_s
=
if
since
,
do
:
Timex.Format.Duration.Formatter
.
lformat
(
since_duration
,
"fr"
,
:humanized
)
{
total_volumes
,
total_gl
}
=
user_stats
(
state
,
nick
)
%{
active
:
active
,
last_at
:
last_at
,
last_cl
:
last_cl
,
last_deg
:
last_deg
,
last_points
:
last_points
,
last_type
:
last_type
,
last_descr
:
last_descr
,
trend_symbol
:
trend
,
active5m
:
m5
,
active15m
:
m15
,
active30m
:
m30
,
active1h
:
h1
,
rising
:
rising
,
active_drinks
:
active_drinks
,
user_status
:
user_status
,
daily_gl
:
total_gl
,
daily_volumes
:
total_volumes
,
sober_in
:
minutes_til_sober
,
sober_in_s
:
sober_in_s
,
since
:
since
,
since_min
:
since_diff
,
since_s
:
since_s
,
}
_
->
nil
end
end
def
handle_info
({
:irc
,
:trigger
,
"sobre"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
args
,
type
:
:dot
}}},
state
)
do
nicks
=
Nola.Membership
.
expanded_members_or_friends
(
m
.
account
,
m
.
network
,
m
.
channel
)
|>
Enum
.
map
(
fn
({
account
,
_
,
nick
})
->
{
nick
,
get_full_statistics
(
state
,
account
.
id
)}
end
)
|>
Enum
.
filter
(
fn
({
_nick
,
status
})
->
status
&&
status
.
sober_in
&&
status
.
sober_in
>
0
end
)
|>
Enum
.
sort_by
(
fn
({
_
,
status
})
->
status
.
sober_in
end
,
&</
2
)
|>
Enum
.
map
(
fn
({
nick
,
stats
})
->
now
=
DateTime
.
utc_now
()
sober
=
now
|>
DateTime
.
add
(
round
(
stats
.
sober_in
*
60
),
:second
)
|>
Timex.Timezone
.
convert
(
"Europe/Paris"
)
at
=
if
now
.
day
==
sober
.
day
do
{
:ok
,
detail
}
=
Timex.Format.DateTime.Formatters.Default
.
lformat
(
sober
,
"aujourd'hui {h24}:{m}"
,
"fr"
)
detail
else
{
:ok
,
detail
}
=
Timex.Format.DateTime.Formatters.Default
.
lformat
(
sober
,
"{WDfull} {h24}:{m}"
,
"fr"
)
detail
end
"
#{
nick
}
sobre
#{
at
}
(dans
#{
stats
.
sober_in_s
}
)"
end
)
|>
Enum
.
intersperse
(
", "
)
|>
Enum
.
join
(
""
)
|>
(
fn
(
line
)
->
case
line
do
""
->
"tout le monde est sobre......."
line
->
line
end
end
)
.
()
|>
m
.
replyfun
.
()
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"sobre"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
args
,
type
:
:bang
}}},
state
)
do
account
=
case
args
do
[
nick
]
->
Nola.Account
.
find_always_by_nick
(
m
.
network
,
m
.
channel
,
nick
)
[]
->
m
.
account
end
if
account
do
user
=
Nola.UserTrack
.
find_by_account
(
m
.
network
,
account
)
nick
=
if
(
user
,
do
:
user
.
nick
,
else
:
account
.
name
)
stats
=
get_full_statistics
(
state
,
account
.
id
)
if
stats
&&
stats
.
sober_in
>
0
do
now
=
DateTime
.
utc_now
()
sober
=
now
|>
DateTime
.
add
(
round
(
stats
.
sober_in
*
60
),
:second
)
|>
Timex.Timezone
.
convert
(
"Europe/Paris"
)
at
=
if
now
.
day
==
sober
.
day
do
{
:ok
,
detail
}
=
Timex.Format.DateTime.Formatters.Default
.
lformat
(
sober
,
"aujourd'hui {h24}:{m}"
,
"fr"
)
detail
else
{
:ok
,
detail
}
=
Timex.Format.DateTime.Formatters.Default
.
lformat
(
sober
,
"{WDfull} {h24}:{m}"
,
"fr"
)
detail
end
m
.
replyfun
.
(
"
#{
nick
}
sera sobre
#{
at
}
(dans
#{
stats
.
sober_in_s
}
)!"
)
else
m
.
replyfun
.
(
"
#{
nick
}
est déjà sobre. aidez le !"
)
end
else
m
.
replyfun
.
(
"inconnu"
)
end
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"alcoolisme"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
[],
type
:
:dot
}}},
state
)
do
nicks
=
Nola.Membership
.
expanded_members_or_friends
(
m
.
account
,
m
.
network
,
m
.
channel
)
|>
Enum
.
map
(
fn
({
account
,
_
,
nick
})
->
{
nick
,
get_full_statistics
(
state
,
account
.
id
)}
end
)
|>
Enum
.
filter
(
fn
({
_nick
,
status
})
->
status
&&
(
status
.
active
>
0
||
status
.
active30m
>
0
)
end
)
|>
Enum
.
sort_by
(
fn
({
_
,
status
})
->
status
.
active
end
,
&>/
2
)
|>
Enum
.
map
(
fn
({
nick
,
status
})
->
trend_symbol
=
if
status
.
active_drinks
>
1
do
Enum
.
join
(
for
(
_
<-
1
..
status
.
active_drinks
,
do
:
status
.
trend_symbol
))
else
status
.
trend_symbol
end
since_str
=
if
status
.
since_min
>
180
do
"depuis:
#{
status
.
since_s
}
| "
else
""
end
"
#{
nick
}
#{
status
.
user_status
}
#{
trend_symbol
}
#{
Float
.
round
(
status
.
active
,
4
)
}
g/l [
#{
since_str
}
sobre dans:
#{
status
.
sober_in_s
}
]"
end
)
|>
Enum
.
intersperse
(
", "
)
|>
Enum
.
join
(
""
)
msg
=
if
nicks
==
""
do
"wtf?!?! personne n'a bu!"
else
nicks
end
m
.
replyfun
.
(
msg
)
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"alcoolisme"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
[
time
],
type
:
:dot
}}},
state
)
do
time
=
case
time
do
"semaine"
->
7
string
->
case
Integer
.
parse
(
string
)
do
{
time
,
"j"
}
->
time
{
time
,
"J"
}
->
time
_
->
nil
end
end
if
time
do
aday
=
time
*
((
24
*
60
)
*
60
)
now
=
DateTime
.
utc_now
()
before
=
now
|>
DateTime
.
add
(
-
aday
,
:second
)
|>
DateTime
.
to_unix
(
:millisecond
)
over_time_stats
(
before
,
time
,
m
,
state
)
else
m
.
replyfun
.
(
".alcooolisme semaine|Xj"
)
end
{
:noreply
,
state
}
end
def
user_over_time
(
account
,
count
)
do
user_over_time
(
data_state
(),
account
,
count
)
end
def
user_over_time
(
state
,
account
,
count
)
do
delay
=
count
*
((
24
*
60
)
*
60
)
now
=
DateTime
.
utc_now
()
before
=
DateTime
.
utc_now
()
|>
DateTime
.
shift_zone!
(
"Europe/Paris"
,
Tzdata.TimeZoneDatabase
)
|>
DateTime
.
add
(
-
delay
,
:second
,
Tzdata.TimeZoneDatabase
)
|>
DateTime
.
to_unix
(
:millisecond
)
#[
# {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
# [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]}
#]
match
=
[{{{
:"$1"
,
:"$2"
},
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
},
[{
:andalso
,
{
:>
,
:"$2"
,
{
:const
,
before
}},
{
:==
,
:"$1"
,
{
:const
,
account
.
id
}}}],
[
:"$_"
]}
]
:ets
.
select
(
state
.
ets
,
match
)
|>
Enum
.
reduce
(
Map
.
new
,
fn
({{
_
,
ts
},
vol
,
_
,
_
,
_
,
_
,
_
,
_
},
acc
)
->
date
=
DateTime
.
from_unix!
(
ts
,
:millisecond
)
|>
DateTime
.
shift_zone!
(
"Europe/Paris"
,
Tzdata.TimeZoneDatabase
)
date
=
if
date
.
hour
<=
8
do
DateTime
.
add
(
date
,
-
(
60
*
(
60
*
(
date
.
hour
+
1
))),
:second
,
Tzdata.TimeZoneDatabase
)
else
date
end
|>
DateTime
.
to_date
()
Map
.
put
(
acc
,
date
,
Map
.
get
(
acc
,
date
,
0
)
+
vol
)
end
)
end
def
user_over_time_gl
(
account
,
count
)
do
state
=
data_state
()
meta
=
get_user_meta
(
state
,
account
.
id
)
delay
=
count
*
((
24
*
60
)
*
60
)
now
=
DateTime
.
utc_now
()
before
=
DateTime
.
utc_now
()
|>
DateTime
.
shift_zone!
(
"Europe/Paris"
,
Tzdata.TimeZoneDatabase
)
|>
DateTime
.
add
(
-
delay
,
:second
,
Tzdata.TimeZoneDatabase
)
|>
DateTime
.
to_unix
(
:millisecond
)
#[
# {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
# [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]}
#]
match
=
[{{{
:"$1"
,
:"$2"
},
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
},
[{
:andalso
,
{
:>
,
:"$2"
,
{
:const
,
before
}},
{
:==
,
:"$1"
,
{
:const
,
account
.
id
}}}],
[
:"$_"
]}
]
:ets
.
select
(
state
.
ets
,
match
)
|>
Enum
.
reduce
(
Map
.
new
,
fn
({{
_
,
ts
},
vol
,
_
,
_
,
_
,
_
,
_
,
_
},
acc
)
->
date
=
DateTime
.
from_unix!
(
ts
,
:millisecond
)
|>
DateTime
.
shift_zone!
(
"Europe/Paris"
,
Tzdata.TimeZoneDatabase
)
date
=
if
date
.
hour
<=
8
do
DateTime
.
add
(
date
,
-
(
60
*
(
60
*
(
date
.
hour
+
1
))),
:second
,
Tzdata.TimeZoneDatabase
)
else
date
end
|>
DateTime
.
to_date
()
weight
=
meta
.
weight
k
=
if
meta
.
sex
,
do
:
0.7
,
else
:
0.6
gl
=
(
10
*
vol
)
/
(
k
*
weight
)
Map
.
put
(
acc
,
date
,
Map
.
get
(
acc
,
date
,
0
)
+
gl
)
end
)
end
defp
over_time_stats
(
before
,
j
,
m
,
state
)
do
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _, _, _, _}) when date > before -> obj end)
match
=
[{{{
:_
,
:"$1"
},
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
},
[{
:>
,
:"$1"
,
{
:const
,
before
}}],
[
:"$_"
]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
members
=
Nola.Membership
.
members_or_friends
(
m
.
account
,
m
.
network
,
m
.
channel
)
drinks
=
:ets
.
select
(
state
.
ets
,
match
)
|>
Enum
.
filter
(
fn
({{
account
,
_
},
_
,
_
,
_
,
_
,
_
,
_
,
_
})
->
Enum
.
member?
(
members
,
account
)
end
)
|>
Enum
.
sort_by
(
fn
({{
_
,
ts
},
_
,
_
,
_
,
_
,
_
,
_
,
_
})
->
ts
end
,
&>/
2
)
top
=
Enum
.
reduce
(
drinks
,
%{},
fn
({{
nick
,
_
},
vol
,
_
,
_
,
_
,
_
,
_
,
_
},
acc
)
->
all
=
Map
.
get
(
acc
,
nick
,
0
)
Map
.
put
(
acc
,
nick
,
all
+
vol
)
end
)
|>
Enum
.
sort_by
(
fn
({
_nick
,
count
})
->
count
end
,
&>/
2
)
|>
Enum
.
map
(
fn
({
nick
,
count
})
->
account
=
Nola.Account
.
get
(
nick
)
user
=
Nola.UserTrack
.
find_by_account
(
m
.
network
,
account
)
nick
=
if
(
user
,
do
:
user
.
nick
,
else
:
account
.
name
)
"
#{
nick
}
:
#{
Float
.
round
(
count
,
4
)
}
"
end
)
|>
Enum
.
intersperse
(
", "
)
m
.
replyfun
.
(
"sur
#{
j
}
jours:
#{
top
}
"
)
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"alcoolisme"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
[],
type
:
:plus
}}},
state
)
do
meta
=
get_user_meta
(
state
,
m
.
account
.
id
)
hf
=
if
meta
.
sex
,
do
:
"h"
,
else
:
"f"
m
.
replyfun
.
(
"+alcoolisme sexe:
#{
hf
}
poids:
#{
meta
.
weight
}
facteur de perte:
#{
meta
.
loss_factor
}
"
)
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"alcoolisme"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
[
h
,
weight
|
rest
],
type
:
:plus
}}},
state
)
do
h
=
case
h
do
"h"
->
true
"f"
->
false
_
->
nil
end
weight
=
case
Util
.
float_paparse
(
weight
)
do
{
weight
,
_
}
->
weight
_
->
nil
end
{
factor
}
=
case
rest
do
[
factor
]
->
case
Util
.
float_paparse
(
factor
)
do
{
float
,
_
}
->
{
float
}
_
->
{
@default_user_meta
.
loss_factor
}
end
_
->
{
@default_user_meta
.
loss_factor
}
end
if
h
==
nil
||
weight
==
nil
do
m
.
replyfun
.
(
"paramètres invalides"
)
else
old_meta
=
get_user_meta
(
state
,
m
.
account
.
id
)
meta
=
Map
.
merge
(
@default_user_meta
,
%{
sex
:
h
,
weight
:
weight
,
loss_factor
:
factor
})
put_user_meta
(
state
,
m
.
account
.
id
,
meta
)
cond
do
old_meta
.
weight
<
meta
.
weight
->
Nola.Plugins.Txt
.
reply_random
(
m
,
"alcoolog.fatter"
)
old_meta
.
weight
==
meta
.
weight
->
m
.
replyfun
.
(
"aucun changement!"
)
true
->
Nola.Plugins.Txt
.
reply_random
(
m
,
"alcoolog.thinner"
)
end
end
{
:noreply
,
state
}
end
def
handle_info
({
:irc
,
:trigger
,
"santai"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
args
,
type
:
:minus
}}},
state
)
do
case
get_statistics_for_nick
(
state
,
m
.
account
.
id
)
do
{
_
,
obj
=
{
_
,
date
,
points
,
_last_active
,
_cl
,
_deg
,
type
,
descr
,
_meta
}}
->
:dets
.
delete_object
(
state
.
dets
,
obj
)
:ets
.
delete
(
state
.
ets
,
{
m
.
account
.
id
,
date
})
m
.
replyfun
.
(
"supprimé:
#{
m
.
sender
.
nick
}
#{
points
}
#{
type
}
#{
descr
}
"
)
Nola.Plugins.Txt
.
reply_random
(
m
,
"alcoolog.delete"
)
notify
=
Nola.Membership
.
notify_channels
(
m
.
account
)
--
[{
m
.
network
,
m
.
channel
}]
for
{
net
,
chan
}
<-
notify
do
user
=
Nola.UserTrack
.
find_by_account
(
net
,
m
.
account
)
nick
=
if
(
user
,
do
:
user
.
nick
,
else
:
m
.
account
.
name
)
Nola.Irc.Connection
.
broadcast_message
(
net
,
chan
,
"
#{
nick
}
-santai
#{
points
}
#{
type
}
#{
descr
}
"
)
end
{
:noreply
,
state
}
_
->
{
:noreply
,
state
}
end
end
def
handle_info
({
:irc
,
:trigger
,
"alcoolisme"
,
m
=
%
Nola.Message
{
trigger
:
%
Nola.Trigger
{
args
:
args
,
type
:
:bang
}}},
state
)
do
{
account
,
duration
}
=
case
args
do
[
nick
|
rest
]
->
{
Nola.Account
.
find_always_by_nick
(
m
.
network
,
m
.
channel
,
nick
),
rest
}
[]
->
{
m
.
account
,
[]}
end
if
account
do
duration
=
case
duration
do
[
"semaine"
]
->
7
[
j
]
->
case
Integer
.
parse
(
j
)
do
{
j
,
"j"
}
->
j
_
->
nil
end
_
->
nil
end
user
=
Nola.UserTrack
.
find_by_account
(
m
.
network
,
account
)
nick
=
if
(
user
,
do
:
user
.
nick
,
else
:
account
.
name
)
if
duration
do
if
duration
>
90
do
m
.
replyfun
.
(
"trop gros, ça rentrera pas"
)
else
# duration stats
stats
=
user_over_time
(
state
,
account
,
duration
)
|>
Enum
.
sort_by
(
fn
({
k
,
_v
})
->
k
end
,
{
:asc
,
Date
})
|>
Enum
.
map
(
fn
({
date
,
count
})
->
"
#{
date
.
day
}
:
#{
Float
.
round
(
count
,
2
)
}
"
end
)
|>
Enum
.
intersperse
(
", "
)
|>
Enum
.
join
(
""
)
if
stats
==
""
do
m
.
replyfun
.
(
"alcoolisme a zéro sur
#{
duration
}
j :/"
)
else
m
.
replyfun
.
(
"alcoolisme de
#{
nick
}
,
#{
duration
}
derniers jours:
#{
stats
}
"
)
end
end
else
if
stats
=
get_full_statistics
(
state
,
account
.
id
)
do
trend_symbol
=
if
stats
.
active_drinks
>
1
do
Enum
.
join
(
for
(
_
<-
1
..
stats
.
active_drinks
,
do
:
stats
.
trend_symbol
))
else
stats
.
trend_symbol
end
# TODO: Lookup nick for account_id
msg
=
"
#{
nick
}
#{
stats
.
user_status
}
"
<>
(
if
stats
.
active
>
0
||
stats
.
active15m
>
0
||
stats
.
active30m
>
0
||
stats
.
active1h
>
0
,
do
:
":
#{
trend_symbol
}
#{
Float
.
round
(
stats
.
active
,
4
)
}
g/l "
,
else
:
""
)
<>
(
if
stats
.
active30m
>
0
||
stats
.
active1h
>
0
,
do
:
"(15m:
#{
stats
.
active15m
}
, 30m:
#{
stats
.
active30m
}
, 1h:
#{
stats
.
active1h
}
) "
,
else
:
""
)
<>
(
if
stats
.
sober_in
>
0
,
do
:
"— Sobre dans
#{
stats
.
sober_in_s
}
"
,
else
:
""
)
<>
(
if
stats
.
since
&&
stats
.
since_min
>
180
,
do
:
"— Paitai depuis
#{
stats
.
since_s
}
"
,
else
:
""
)
<>
"— Dernier verre:
#{
present_type
(
stats
.
last_type
,
stats
.
last_descr
)
}
[
#{
Float
.
round
(
stats
.
last_points
+
0.0
,
4
)
}
] "
<>
"
#{
format_duration_from_now
(
stats
.
last_at
)
}
"
<>
(
if
stats
.
daily_volumes
>
0
,
do
:
"— Aujourd'hui:
#{
stats
.
daily_volumes
}
#{
stats
.
daily_gl
}
g/l"
,
else
:
""
)
m
.
replyfun
.
(
msg
)
else
m
.
replyfun
.
(
"honteux mais
#{
nick
}
n'a pas l'air alcoolique du tout. /kick"
)
end
end
else
m
.
replyfun
.
(
"je ne connais pas cet utilisateur"
)
end
{
:noreply
,
state
}
end
# Account merge
def
handle_info
({
:account_change
,
old_id
,
new_id
},
state
)
do
spec
=
[{{
:"$1"
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
},
[{
:==
,
:"$1"
,
{
:const
,
old_id
}}],
[
:"$_"
]}]
Util
.
ets_mutate_select_each
(
:dets
,
state
.
dets
,
spec
,
fn
(
table
,
obj
)
->
Logger
.
debug
(
"alcolog/account_change:: merging
#{
old_id
}
->
#{
new_id
}
"
)
rename_object_owner
(
table
,
state
.
ets
,
obj
,
old_id
,
new_id
)
end
)
case
:dets
.
lookup
(
state
.
meta
,
{
:meta
,
old_id
})
do
[{
_
,
meta
}]
->
:dets
.
delete
(
state
.
meta
,
{
:meta
,
old_id
})
:dets
.
insert
(
state
.
meta
,
{{
:meta
,
new_id
},
meta
})
_
->
:ok
end
{
:noreply
,
state
}
end
def
terminate
(
_
,
state
)
do
for
dets
<-
[
state
.
dets
,
state
.
meta
]
do
:dets
.
sync
(
dets
)
:dets
.
close
(
dets
)
end
end
defp
rename_object_owner
(
table
,
ets
,
object
=
{
old_id
,
date
,
volume
,
current
,
cl
,
deg
,
name
,
comment
,
meta
},
old_id
,
new_id
)
do
:dets
.
delete_object
(
table
,
object
)
:ets
.
delete
(
ets
,
{
old_id
,
date
})
:dets
.
insert
(
table
,
{
new_id
,
date
,
volume
,
current
,
cl
,
deg
,
name
,
comment
,
meta
})
:ets
.
insert
(
ets
,
{{
new_id
,
date
},
volume
,
current
,
cl
,
deg
,
name
,
comment
,
meta
})
end
# Account: move from nick to account id
def
handle_info
({
:accounts
,
accounts
},
state
)
do
#for x={:account, _, _, _, _} <- accounts, do: handle_info(x, state)
#{:noreply, state}
mapping
=
Enum
.
reduce
(
accounts
,
Map
.
new
,
fn
({
:account
,
_net
,
_chan
,
nick
,
account_id
},
acc
)
->
Map
.
put
(
acc
,
String
.
downcase
(
nick
),
account_id
)
end
)
spec
=
[{{
:"$1"
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
},
[],
[
:"$_"
]}]
Logger
.
debug
(
"accounts:: mappings
#{
inspect
mapping
}
"
)
Util
.
ets_mutate_select_each
(
:dets
,
state
.
dets
,
spec
,
fn
(
table
,
obj
=
{
nick
,
_date
,
_vol
,
_cur
,
_cl
,
_deg
,
_name
,
_comment
,
_meta
})
->
#Logger.debug("accounts:: item #{inspect(obj)}")
if
new_id
=
Map
.
get
(
mapping
,
nick
)
do
Logger
.
debug
(
"alcolog/accounts:: merging
#{
nick
}
->
#{
new_id
}
"
)
rename_object_owner
(
table
,
state
.
ets
,
obj
,
nick
,
new_id
)
end
end
)
{
:noreply
,
state
}
end
def
handle_info
({
:account
,
_net
,
_chan
,
nick
,
account_id
},
state
)
do
nick
=
String
.
downcase
(
nick
)
spec
=
[{{
:"$1"
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
},
[{
:==
,
:"$1"
,
{
:const
,
nick
}}],
[
:"$_"
]}]
Util
.
ets_mutate_select_each
(
:dets
,
state
.
dets
,
spec
,
fn
(
table
,
obj
)
->
Logger
.
debug
(
"alcoolog/account:: merging
#{
nick
}
->
#{
account_id
}
"
)
rename_object_owner
(
table
,
state
.
ets
,
obj
,
nick
,
account_id
)
end
)
case
:dets
.
lookup
(
state
.
meta
,
{
:meta
,
nick
})
do
[{
_
,
meta
}]
->
:dets
.
delete
(
state
.
meta
,
{
:meta
,
nick
})
:dets
.
insert
(
state
.
meta
,
{{
:meta
,
account_id
},
meta
})
_
->
:ok
end
{
:noreply
,
state
}
end
def
handle_info
(
t
,
state
)
do
Logger
.
debug
(
"
#{
__MODULE__
}
: unhandled info
#{
inspect
t
}
"
)
{
:noreply
,
state
}
end
def
nick_history
(
account
)
do
spec
=
[
{{{
:"$1"
,
:_
},
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
},
[{
:==
,
:"$1"
,
{
:const
,
account
.
id
}}],
[
:"$_"
]}
]
:ets
.
select
(
data_state
()
.
ets
,
spec
)
end
defp
get_statistics_for_nick
(
state
,
account_id
)
do
qvc
=
:dets
.
lookup
(
state
.
dets
,
account_id
)
|>
Enum
.
sort_by
(
fn
({
_
,
ts
,
_
,
_
,
_
,
_
,
_
,
_
,
_
})
->
ts
end
,
&</
2
)
count
=
Enum
.
reduce
(
qvc
,
0
,
fn
({
_nick
,
_ts
,
points
,
_active
,
_cl
,
_deg
,
_type
,
_descr
,
_meta
},
acc
)
->
acc
+
(
points
||
0
)
end
)
last
=
List
.
last
(
qvc
)
||
nil
{
count
,
last
}
end
def
present_type
(
type
,
descr
)
when
descr
in
[
nil
,
""
],
do
:
"
#{
type
}
"
def
present_type
(
type
,
description
),
do
:
"
#{
type
}
(
#{
description
}
)"
def
format_points
(
int
)
when
is_integer
(
int
)
and
int
>
0
do
"+
#{
Integer
.
to_string
(
int
)
}
"
end
def
format_points
(
int
)
when
is_integer
(
int
)
and
int
<
0
do
Integer
.
to_string
(
int
)
end
def
format_points
(
int
)
when
is_float
(
int
)
and
int
>
0
do
"+
#{
Float
.
to_string
(
Float
.
round
(
int
,
4
))
}
"
end
def
format_points
(
int
)
when
is_float
(
int
)
and
int
<
0
do
Float
.
to_string
(
Float
.
round
(
int
,
4
))
end
def
format_points
(
0
),
do
:
"0"
def
format_points
(
0.0
),
do
:
"0"
defp
format_relative_timestamp
(
timestamp
)
do
alias
Timex.Format.DateTime.Formatters
alias
Timex.Timezone
date
=
timestamp
|>
DateTime
.
from_unix!
(
:millisecond
)
|>
Timezone
.
convert
(
"Europe/Paris"
)
{
:ok
,
relative
}
=
Formatters.Relative
.
relative_to
(
date
,
Timex
.
now
(
"Europe/Paris"
),
"{relative}"
,
"fr"
)
{
:ok
,
detail
}
=
Formatters.Default
.
lformat
(
date
,
" ({h24}:{m})"
,
"fr"
)
relative
<>
detail
end
def
put_user_meta
(
state
,
account_id
,
meta
)
do
:dets
.
insert
(
state
.
meta
,
{{
:meta
,
account_id
},
meta
})
:ok
end
def
get_user_meta
(%{
meta
:
meta
},
account_id
)
do
case
:dets
.
lookup
(
meta
,
{
:meta
,
account_id
})
do
[{{
:meta
,
_
},
meta
}]
->
Map
.
merge
(
@default_user_meta
,
meta
)
_
->
@default_user_meta
end
end
# Calcul g/l actuel:
# 1. load user meta
# 2. foldr ets
# for each object
# get_current_alcohol
# ((object g/l) - 0,15/l/60)* minutes_since_drink
# if minutes_since_drink < 10, reduce g/l (?!)
# acc + current_alcohol
# stop folding when ?
#
def
user_stats
(
account
)
do
user_stats
(
data_state
(),
account
.
id
)
end
defp
user_stats
(
state
=
%{
ets
:
ets
},
account_id
)
do
meta
=
get_user_meta
(
state
,
account_id
)
aday
=
(
10
*
60
)
*
60
now
=
DateTime
.
utc_now
()
before
=
now
|>
DateTime
.
add
(
-
aday
,
:second
)
|>
DateTime
.
to_unix
(
:millisecond
)
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
match
=
[
{{{
:"$1"
,
:"$2"
},
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
},
[
{
:>
,
:"$2"
,
{
:const
,
before
}},
{
:"=:="
,
{
:const
,
account_id
},
:"$1"
}
],
[
:"$_"
]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
drinks
=
:ets
.
select
(
ets
,
match
)
# {date, single_peak}
total_volume
=
Enum
.
reduce
(
drinks
,
0.0
,
fn
({{
_
,
date
},
volume
,
_
,
_
,
_
,
_
,
_
,
_
},
acc
)
->
acc
+
volume
end
)
k
=
if
meta
.
sex
,
do
:
0.7
,
else
:
0.6
weight
=
meta
.
weight
gl
=
(
10
*
total_volume
)
/
(
k
*
weight
)
{
Float
.
round
(
total_volume
+
0.0
,
4
),
Float
.
round
(
gl
+
0.0
,
4
)}
end
defp
alcohol_level_rising
(
state
,
account_id
,
minutes
\\
30
)
do
{
now
,
_
}
=
current_alcohol_level
(
state
,
account_id
)
soon_date
=
DateTime
.
utc_now
|>
DateTime
.
add
(
minutes
*
60
,
:second
)
{
soon
,
_
}
=
current_alcohol_level
(
state
,
account_id
,
soon_date
)
soon
=
cond
do
soon
<
0
->
0.0
true
->
soon
end
#IO.puts "soon #{soon_date} - #{inspect soon} #{inspect now}"
{
soon
>
now
,
Float
.
round
(
soon
+
0.0
,
4
)}
end
defp
current_alcohol_level
(
state
=
%{
ets
:
ets
},
account_id
,
now
\\
nil
)
do
meta
=
get_user_meta
(
state
,
account_id
)
aday
=
((
24
*
7
)
*
60
)
*
60
now
=
if
now
do
now
else
DateTime
.
utc_now
()
end
before
=
now
|>
DateTime
.
add
(
-
aday
,
:second
)
|>
DateTime
.
to_unix
(
:millisecond
)
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
match
=
[
{{{
:"$1"
,
:"$2"
},
:_
,
:_
,
:_
,
:_
,
:_
,
:_
,
:_
},
[
{
:>
,
:"$2"
,
{
:const
,
before
}},
{
:"=:="
,
{
:const
,
account_id
},
:"$1"
}
],
[
:"$_"
]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
drinks
=
:ets
.
select
(
ets
,
match
)
|>
Enum
.
sort_by
(
fn
({{
_
,
date
},
_
,
_
,
_
,
_
,
_
,
_
,
_
})
->
date
end
,
&</
2
)
# {date, single_peak}
{
all
,
last_drink_at
,
gl
,
active_drinks
}
=
Enum
.
reduce
(
drinks
,
{
0.0
,
nil
,
[],
0
},
fn
({{
_
,
date
},
volume
,
_
,
_
,
_
,
_
,
_
,
_
},
{
all
,
last_at
,
acc
,
active_drinks
})
->
k
=
if
meta
.
sex
,
do
:
0.7
,
else
:
0.6
weight
=
meta
.
weight
peak
=
(
10
*
volume
)
/
(
k
*
weight
)
date
=
case
date
do
ts
when
is_integer
(
ts
)
->
DateTime
.
from_unix!
(
ts
,
:millisecond
)
date
=
%
NaiveDateTime
{}
->
DateTime
.
from_naive!
(
date
,
"Etc/UTC"
)
date
=
%
DateTime
{}
->
date
end
last_at
=
last_at
||
date
mins_since
=
round
(
DateTime
.
diff
(
now
,
date
)
/
60.0
)
#IO.puts "Drink: #{inspect({date, volume})} - mins since: #{inspect mins_since} - last drink at #{inspect last_at}"
# Apply loss since `last_at` on `all`
#
all
=
if
last_at
do
mins_since_last
=
round
(
DateTime
.
diff
(
date
,
last_at
)
/
60.0
)
loss
=
((
meta
.
loss_factor
/
100
)
/
60
)
*
(
mins_since_last
)
#IO.puts "Applying last drink loss: from #{all}, loss of #{inspect loss} (mins since #{inspect mins_since_first})"
cond
do
(
all
-
loss
)
>
0
->
all
-
loss
true
->
0.0
end
else
all
end
#IO.puts "Applying last drink current before drink: #{inspect all}"
if
mins_since
<
30
do
per_min
=
(
peak
)
/
30.0
current
=
(
per_min
*
mins_since
)
#IO.puts "Applying current drink 30m: from #{peak}, loss of #{inspect per_min}/min (mins since #{inspect mins_since})"
{
all
+
current
,
date
,
[{
date
,
current
}
|
acc
],
active_drinks
+
1
}
else
{
all
+
peak
,
date
,
[{
date
,
peak
}
|
acc
],
active_drinks
}
end
end
)
#IO.puts "last drink #{inspect last_drink_at}"
mins_since_last
=
if
last_drink_at
do
round
(
DateTime
.
diff
(
now
,
last_drink_at
)
/
60.0
)
else
0
end
# Si on a déjà bu y'a déjà moins 15 minutes (big up le binge drinking), on applique plus de perte
level
=
if
mins_since_last
>
15
do
loss
=
((
meta
.
loss_factor
/
100
)
/
60
)
*
(
mins_since_last
)
Float
.
round
(
all
-
loss
,
4
)
else
all
end
#IO.puts "\n LEVEL #{inspect level}\n\n\n\n"
cond
do
level
<
0
->
{
0.0
,
0
}
true
->
{
level
,
active_drinks
}
end
end
defp
format_duration_from_now
(
date
,
with_detail
\\
true
)
do
date
=
if
is_integer
(
date
)
do
date
=
DateTime
.
from_unix!
(
date
,
:millisecond
)
|>
Timex.Timezone
.
convert
(
"Europe/Paris"
)
else
Util
.
to_naive_date_time
(
date
)
end
now
=
DateTime
.
utc_now
()
|>
Timex.Timezone
.
convert
(
"Europe/Paris"
)
{
:ok
,
detail
}
=
Timex.Format.DateTime.Formatters.Default
.
lformat
(
date
,
"({h24}:{m})"
,
"fr"
)
mins_since
=
round
(
DateTime
.
diff
(
now
,
date
)
/
60.0
)
if
ago
=
format_minute_duration
(
mins_since
)
do
word
=
if
mins_since
>
0
do
"il y a "
else
"dans "
end
word
<>
ago
<>
if
(
with_detail
,
do
:
"
#{
detail
}
"
,
else
:
""
)
else
"maintenant
#{
detail
}
"
end
end
defp
format_minute_duration
(
minutes
)
do
sober_in_s
=
if
(
minutes
!=
0
)
do
duration
=
Timex.Duration
.
from_minutes
(
minutes
)
Timex.Format.Duration.Formatter
.
lformat
(
duration
,
"fr"
,
:humanized
)
else
nil
end
end
end
File Metadata
Details
Attached
Mime Type
text/x-ruby
Expires
Mon, Jul 7, 5:25 AM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
49859
Default Alt Text
alcoolog.ex (46 KB)
Attached To
rNOLA Nola
Event Timeline
Log In to Comment