diff --git a/config/config.exs b/config/config.exs index 370867c..fb22fe2 100644 --- a/config/config.exs +++ b/config/config.exs @@ -6,4 +6,7 @@ config :logger, handle_otp_reports: true, handle_sasl_reports: true +config :cronex, + timezone: "Europe/Copenhagen" + import_config "#{Mix.env}.secret.exs" diff --git a/config/prod.secret.exs.example b/config/prod.secret.exs.example index 9a56869..a071519 100644 --- a/config/prod.secret.exs.example +++ b/config/prod.secret.exs.example @@ -6,7 +6,10 @@ use Mix.Config config :slack, token: "xoxb", owner: "cdetroye", + github: "https://github.com/m1dnight/slackbot", benvolios_owner: "cdetroye", benvolios_channel: "benvolios", + diswasher_duties_owner: "humberto", + diswasher_duties_channel: "dishwasher_duties", rss_channel: "bots" diff --git a/data/dishwasher_manager/schedule.dat b/data/dishwasher_manager/schedule.dat new file mode 100644 index 0000000..e7c64ed --- /dev/null +++ b/data/dishwasher_manager/schedule.dat @@ -0,0 +1,85 @@ +[{cderoove,{<<"Coen De Roover">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 11,month => 9,year => 2017}}}, + {mathsaey,{<<"Mathijs Saey">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 18,month => 9,year => 2017}}}, + {thierry,{<<"Thierry Renaux">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 25,month => 9,year => 2017}}}, + {kevin,{<<"Kevin Pinte">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 2,month => 10,year => 2017}}}, + {fmyter,{<<"Florian Myter">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 9,month => 10,year => 2017}}}, + {tmoldere,{<<"Tim Molderez">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 16,month => 10,year => 2017}}}, + {joeri,{<<"Joeri De Koster">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 23,month => 10,year => 2017}}}, + {simon,{<<"Simon Van de Water">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 30,month => 10,year => 2017}}}, + {nathalie,{<<"Nathalie Oostvogels">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 6,month => 11,year => 2017}}}, + {jswalens,{<<"Janwillem Swalens">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 13,month => 11,year => 2017}}}, + {eljenso,{<<"Jens Nicolay">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 20,month => 11,year => 2017}}}, + {kennedy,{<<"Kennedy Kambona">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 27,month => 11,year => 2017}}}, + {qstieven,{<<"Quentin Stievenart">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 4,month => 12,year => 2017}}}, + {dirk,{<<"Dirk van Deun">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 11,month => 12,year => 2017}}}, + {jbsartor,{<<"Jennifer Sartor">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 18,month => 12,year => 2017}}}, + {jdeblese,{<<"Jonas De Bleser">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 8,month => 1,year => 2018}}}, + {nvgeele,{<<"Nils Van Geele">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 15,month => 1,year => 2018}}}, + {ascullpu,{<<"Angel Luis Scull Pupo">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 22,month => 1,year => 2018}}}, + {carmen,{<<"Carmen Torres Lopez">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 29,month => 1,year => 2018}}}, + {jezaman,{<<"Jesse Zaman">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 5,month => 2,year => 2018}}}, + {mvdcamme,{<<"Maarten Vandercammen">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 12,month => 2,year => 2018}}}, + {lachrist,{<<"Laurent Christophe">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 19,month => 2,year => 2018}}}, + {marmat,{<<"Matteo Marra">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 26,month => 2,year => 2018}}}, + {noah,{<<"Noah Van Es">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 5,month => 3,year => 2018}}}, + {svdvonde,{<<"Sam Van den Vonder">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 12,month => 3,year => 2018}}}, + {humberto,{<<"Humberto Rodriguez">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 19,month => 3,year => 2018}}}, + {cdetroye,{<<"Christophe De Troyer">>, + #{'__struct__' => 'Elixir.Date',calendar => 'Elixir.Calendar.ISO', + day => 26,month => 3,year => 2018}}}, + {elisagboix,{<<"Elisa Gonzalez">>, + #{'__struct__' => 'Elixir.Date', + calendar => 'Elixir.Calendar.ISO',day => 2,month => 4, + year => 2018}}}]. diff --git a/data/dishwasher_manager/users.dat b/data/dishwasher_manager/users.dat new file mode 100644 index 0000000..35f4de8 --- /dev/null +++ b/data/dishwasher_manager/users.dat @@ -0,0 +1,33 @@ +[{'U03K40ETE',<<"Humberto Rodriguez">>}, + {'U08DX089E',<<"Christophe De Troyer">>}, + {'U02D6HWS1',<<"Elisa Gonzalez Boix">>}, + {'U0LV04PPT',<<"Ward Muylaert">>}, + {'U02C3FMQ2',<<"Wolfgang De Meuter">>}, + {'U94HUEHLJ',<<"Thomas Dupriez">>}, + {'U8PGUSQ3F',<<"Dario Di Nucci">>}, + {'U02QPK57T',<<"Coen De Roover">>}, + {'U02J6647B',<<"Mathijs Saey">>}, + {'U02J62LRF',<<"Thierry Renaux">>}, + {'U02BYG1E9',<<"Kevin Pinte">>}, + {'U02MXLBFX',<<"Florian Myter">>}, + {'U03QXV0QJ',<<"Tim Molderez">>}, + {'U03SKNNFB',<<"Joeri De Koster">>}, + {'U21S584KW',<<"Sam Van den Vonder">>}, + {'U02C2C9BD',<<"Nathalie Oostvogels">>}, + {'U02C3M0CL',<<"Janwillem Swalens">>}, + {'U02C36Q3E',<<"Jens Nicolay">>}, + {'U02J62SFM',<<"Kennedy Kambona">>}, + {'U02J7ERTW',<<"Quentin Stievenart">>}, + {'U02C53L03',<<"Dirk van Deun">>}, + {'U21RHEHR7',<<"Jonas De Bleser">>}, + {'U21SQKEFL',<<"Nils Van Geele">>}, + {'U0QSNFR96',<<"Angel Luis Scull Pupo">>}, + {'U15B9LJHL',<<"Carmen Torres">>}, + {'U02C36WBA',<<"Jesse Zaman">>}, + {'U088RMBMY',<<"Maarten Vandercammen">>}, + {'U03U17WCQ',<<"Laurent Christophe">>}, + {'U4U6994KG',<<"Matteo Marra">>}, + {'U6X6VGMF1',<<"Noah Van Es">>}, + {'U21S584KW',<<"Sam Van den Vonder">>}, + {'U6SH5G347',<<"Isaac Oteyo">>}, + {'U758VEE2E',<<"Yunior Pacheco Correa">>}]. diff --git a/lib/behaviours/plugin.ex b/lib/behaviours/plugin.ex index 715febb..d37d16e 100644 --- a/lib/behaviours/plugin.ex +++ b/lib/behaviours/plugin.ex @@ -24,18 +24,29 @@ defmodule Plugin do GenServer.start_link(__MODULE__, [args], [{:name, __MODULE__}]) end + # "Handling indidual messages" + def handle_info(message = %{channel: nil, type: "message", text: text, user: from}, state) do + reply = on_message(text, :dishwasher_app, from) + + case reply do + {:ok, reply} -> SlackManager.send_private_message("#{reply}", from) + {:error, reason} -> IO.puts "Error in plugin #{reason}" + {:noreply} -> :noop + end + + {:noreply, state} + end + def handle_info(message = %{type: "message", text: text, user: from}, state) do reply = on_message(text, message.channel, from) case reply do {:ok, reply} -> SlackManager.send_message("#{reply}", message.channel) - {:error, reason} -> - IO.puts "Error in plugin #{reason}" - {:noreply} -> - :noop + {:error, reason} -> IO.puts "Error in plugin #{reason}" + {:noreply} -> :noop end - + {:noreply, state} end diff --git a/lib/bot/benvolios.ex b/lib/bot/benvolios.ex index a1d2814..76a507a 100644 --- a/lib/bot/benvolios.ex +++ b/lib/bot/benvolios.ex @@ -56,7 +56,7 @@ defmodule Bot.Benvolios do end # Prints out help message. - def on_message(<<"help"::utf8>>, _channel, _sender) do + def on_message(<<"help"::utf8>>, @channel, _sender) do res = """ ``` order : Order something. diff --git a/lib/bot/diswasher_manager.ex b/lib/bot/diswasher_manager.ex new file mode 100644 index 0000000..d2c316a --- /dev/null +++ b/lib/bot/diswasher_manager.ex @@ -0,0 +1,194 @@ +defmodule Bot.DiswasherManager do + require Logger + use Plugin + + @boss Application.fetch_env!(:slack, :diswasher_duties_owner) + @channel Application.fetch_env!(:slack, :diswasher_duties_channel) + @private_channel :dishwasher_app + @channels [@channel, @private_channel ] + + # The message "swap_with @x" is used to swap weekly duties with the user `x`. + def on_message(<<"swap_with"::utf8, user::bitstring>>, _channel, sender) do + user = String.trim(user) + case Brain.DishwasherManager.swap_duties(sender, user) do + :ok -> SlackManager.send_private_message("You swapped your dishwasher duties with @#{sender}.", user) + {:ok, "Duties swapped!"} + {:error, msg} -> {:ok, msg} + end + end + + # The message "manager?" return the name uf the current dishwasher manager + def on_message(<<"manager?"::utf8>>, _channel, _sender) do + {:ok, manager, fullname} = Brain.DishwasherManager.manager?() + + case manager do + :no_specified -> {:ok, "The schedule has not been created. Use the command 'help' for more information."} + _name -> {:ok, "The current dishwasher manager is `#{fullname}`"} + end + end + + def on_message(<<"when?"::utf8>>, _channel, sender) do + {:ok, fullname, startDate} = Brain.DishwasherManager.when?(sender) + + case fullname do + :invalid_user -> {:ok, "The schedule has not been created. Use the command 'help' for more information."} + + _ -> from = Date.to_string(startDate) + to = startDate |> Date.add(4) |> Date.to_string() + {:ok, "Your next dishwasher duties will be from `#{from}` to `#{to}`"} + end + end + + # Prints out all the outstanding orders. + # Restricted to admins only. + def on_message(<<"schedule"::utf8, _::bitstring>>, channel, _sender) when channel in @channels do + {:ok, schedule} = Brain.DishwasherManager.schedule() + build_schedule_list(schedule) + end + + # Prints out all the outstanding orders. + # Restricted to admins only. + def on_message(<<"create_schedule"::utf8, startDate::bitstring>>, channel, @boss) when channel in @channels do + {:ok, schedule} = Brain.DishwasherManager.create_schedule(startDate) + build_schedule_list(schedule) + end + + def on_message(<<"users"::utf8, _::bitstring>>, channel, @boss) when channel in @channels do + {:ok, users} = Brain.DishwasherManager.users() + build_user_list(users) + end + + def on_message(<<"add_user"::utf8, userDetails::bitstring>>, channel, @boss) when channel in @channels do + case String.split(userDetails) do + [] -> {:noreply} + [user| fullname] -> :ok = Brain.DishwasherManager.add_user(user, fullname) + {:ok, "The user {#{user}, #{fullname}} has been saved."} + end + end + + def on_message(<<"remove_user"::utf8, user::bitstring>>, channel, @boss) when channel in @channels do + :ok = Brain.DishwasherManager.remove_user(user) + {:ok, "The user #{user} has been removed."} + end + + def on_message(<<"remove_users"::utf8>>, channel, @boss) when channel in @channels do + :ok = Brain.DishwasherManager.remove_users() + {:ok, "The user list was removed."} + end + + def on_message(<<"remove_schedule"::utf8>>, channel, @boss) when channel in @channels do + :ok = Brain.DishwasherManager.remove_schedule() + {:ok, "The schedule was removed."} + end + + def on_message(<<"set_manager"::utf8>>, channel, @boss) when channel in @channels do + {:ok, manager} = Brain.DishwasherManager.set_manager_of_the_week() + case manager do + :no_specified -> {:ok, "The schedule has not been created. Use the command 'help' for more information."} + name -> {:ok, "The current dishwasher manager is `#{name}`"} + end + end + + def on_message(<<"wave"::utf8>>, channel, _sender) when channel in @channels do + wave_dishwasher_manager() + end + + def on_message(<<":wave:"::utf8>>, channel, _sender) when channel in @channels do + wave_dishwasher_manager() + end + + # Prints out help message. + def on_message(<<"help"::utf8>>, channel, @boss) when channel in @channels do + res = boss_help_menu() + {:ok, res} + end + + # Prints out help message. + def on_message(<<"help"::utf8>>, channel, _sender) when channel in @channels do + res = user_help_menu() + {:ok, res} + end + + def on_message(_text, _channel, _sender) do + {:noreply} + end + + + defp build_schedule_list(schedule) when schedule == %{} , do: {:ok, "There is no schedule ready. Use the command 'help' for more information."} + + defp build_schedule_list(schedule) do + resp = schedule + |> Enum.map(fn {_k,{fullname, from}} -> + "- #{fullname} -> from: #{from} to: #{Date.add(from, 4)}" end) + |> Enum.join("\n") + + {:ok, "```#{resp}```"} + end + + defp build_user_list(users) when users == %{} , do: {:ok, "The list of user is empty. Use the command 'help' for more information."} + + defp build_user_list(users) do + resp = users + |> Enum.map(fn {_, fullname} -> "- #{fullname}" end) + |> Enum.join("\n") + + {:ok, "```#{resp}```"} + end + + defp user_help_menu do + """ + ``` + swap_with : Swaps weekly duties with another person. + Example: "swap_with @cdtroye". + manager? : Shows the current dishwasher manager. + Example: "manager?" + schedule : Shows the current dishwasher schedule. + Example: "schedule" + wave : Sends a notification to the current dishwasher manager in case he has to do his duties. + Example: "wave" or ":wave:" + when? : Shows the dates of your the next dishwasher duties. + Example: "when? " + ``` + + """ + end + + defp boss_help_menu do + """ + ``` + swap_with : Swaps weekly duties with another person. + Example: "swap_with @cdtroye". + manager? : Shows the current dishwasher manager. + Example: "manager?" + schedule : Shows the current dishwasher schedule. + Example: "schedule" + create_schedule : Creates a dishwasher schedule starting from the date specified. + Example: "create_schedule 2017-09-07" + users : Shows the current list of users. + Example: "users" + add_user : Add a new user. The first argument is the "slack user name", + followed by its fullname. + Example: "add_user @humberto Humberto Rodriguez Avila" + remove_user : Remove an user. + Example: "remove_user @humberto" + set_manager : Set the manager for the current week. + Example: "set_manager" + ``` + + """ + end + + + defp wave_dishwasher_manager do + {:ok, manager, fullname} = Brain.DishwasherManager.manager?() + + case manager do + :no_specified -> {:ok, "As the schedule has not been created, there is not Dishwasher Manager."} + _name -> SlackManager.send_private_message(":wave: Hey Dishwasher Manager! Please do your dishwasher duties as soon as you can.", manager) + {:ok, "The current dishwasher manager `#{fullname}` was :wave:"} + end + end + + + +end diff --git a/lib/bot/misc.ex b/lib/bot/misc.ex index 28d95a5..2d386fd 100644 --- a/lib/bot/misc.ex +++ b/lib/bot/misc.ex @@ -7,7 +7,7 @@ defmodule Bot.Misc do {:ok,"My owner is #{@owner}"} end - def on_message(<<"!bug "::utf8, target::bitstring>>, channel, from) do + def on_message(<<"!bug "::utf8, target::bitstring>>, _channel, _from) do {:ok, "#{target}, seems like are not happy with the current operations of the bot. If you feel like this hinders you in any way, feel free to write a patch and submit it on GitHub. You can find the repository at #{@github}"} end diff --git a/lib/brain/dishwasher_manager.ex b/lib/brain/dishwasher_manager.ex new file mode 100644 index 0000000..98b87aa --- /dev/null +++ b/lib/brain/dishwasher_manager.ex @@ -0,0 +1,328 @@ +defmodule Brain.DishwasherManager do + require Logger + use GenServer + + @users_file "data/dishwasher_manager/users.dat" + @users_map_file "data/dishwasher_manager/users-map.dat" + @schedule_file "data/dishwasher_manager/schedule.dat" + @holidays [~D[2017-12-25], ~D[2018-01-01] ] + + def start_link(initial_state \\ []) do + GenServer.start_link __MODULE__, [initial_state], name: __MODULE__ + end + + def init([[]]) do + Logger.debug "No state provided. Reading from disk." + case restore_state() do + {:ok, state} -> + {:ok, state} + _ -> + Logger.warn "No initial state was restored from backup." + {:ok, {%{}, %{}, %{}, :no_specified}} + end + end + + def init([state]) do + {:ok, state} + + end + + ############# + # Interface # + ############# + + def swap_duties(userA, userB) do + GenServer.call __MODULE__, {:swap, userA, userB} + end + + def manager?() do + GenServer.call __MODULE__, :manager? + end + + def next_manager do + GenServer.call __MODULE__, :next_manager + end + + def when?(user) do + GenServer.call __MODULE__, {:when?, user} + end + + def schedule() do + GenServer.call __MODULE__, :schedule + end + + def create_schedule(startDate) do + GenServer.call __MODULE__, {:create_schedule, startDate} + end + + def users() do + GenServer.call __MODULE__, :users + end + + def add_user(user, fullname) do + GenServer.call __MODULE__, {:add_user, user, fullname} + end + + def remove_user(user) do + GenServer.call __MODULE__, {:remove_user, user} + end + + def set_manager_of_the_week() do + GenServer.call __MODULE__, :set_manager_of_the_week + end + + def remove_users() do + GenServer.call __MODULE__, :remove_users + end + + def remove_schedule() do + GenServer.call __MODULE__, :remove_schedule + end + + + ######### + # Calls # + ######### + + def handle_call(:users, _from, {_, users, _, _,_} = state) do + {:reply, {:ok, users}, state} + end + + def handle_call(:manager?, _from, {_, users, idNameMap, _, manager} = state) do + fullName = Keyword.get(users, manager, :no_specified) + user = Map.get(idNameMap, Atom.to_string(manager)) + {:reply, {:ok, user, fullName}, state} + end + + def handle_call(:next_manager, _from, {schedule, users, idNameMap, _, _} = state) do + manager = + case get_next_manager(schedule) do + :no_specified -> {user, _} = List.first users + user + user -> user + end + {:reply, {:ok, Map.get(idNameMap, Atom.to_string(manager))}, state} + end + + def handle_call({:when?, user}, _from, {schedule, _, _, nameIdMap, _} = state) do + userId = Map.get(nameIdMap, user) |> String.to_atom + {fullName, startDate} = Keyword.get(schedule, userId, {:invalid_user, :no_specified}) + {:reply, {:ok, fullName, startDate}, state} + end + + + def handle_call(:set_manager_of_the_week, _from, {schedule, users, idNameMap, nameIdMap, _} ) do + {schedule, manager} = + case get_manager_of_week(schedule) do + :no_specified -> sch = build_schedule(users, Date.utc_today) + man = get_manager_of_week(sch) + {sch, man} + user -> {schedule, user} + end + + fullName = Keyword.get(users, manager, :no_specified) + {:reply, {:ok, fullName}, {schedule, users, idNameMap, nameIdMap, manager}} + end + + def handle_call(:schedule, _from, {schedule, _,_,_,_} = state) do + {:reply, {:ok, schedule}, state} + end + + def handle_call({:create_schedule, startDate}, _from, {_, users, idNameMap, nameIdMap, _}) do + + {:ok, startDate} = startDate + |> String.trim() + |> Date.from_iso8601() + + schedule = build_schedule(users, startDate) + save_state(schedule, @schedule_file) + manager = get_manager_of_week(schedule) + {:reply, {:ok, schedule}, {schedule, users, idNameMap, nameIdMap, manager}} + end + + def handle_call({:add_user, user, fullname}, _from, {schedule, users, idNameMap, nameIdMap, manager}) do + fullname = buildFullName(fullname) + user = user + |> String.trim() + # |> String.to_atom() + + userId = Map.get(nameIdMap, user) + users = Keyword.put_new(users, userId, fullname) + schedule = add_to_schedule(schedule, userId, fullname) + save_state(schedule, @schedule_file) + save_state(users, @users_file) + {:reply, :ok, {schedule, users, idNameMap, nameIdMap, manager}} + end + + + def handle_call({:remove_user, user}, _from, {schedule, users, idNameMap, nameIdMap, manager}) do + user = user + |> String.trim() + # |> String.to_atom() + + userId = Map.get(nameIdMap, user) + userList = Keyword.delete(users, userId) + save_state(userList, @users_file) + + {:reply, :ok, {schedule, userList, idNameMap, nameIdMap, manager}} + end + + def handle_call({:swap, _userA, _userB}, _from, {[], _, _, _, _} = state) do + {:reply, {:error, "There is no schedule ready. Use the command 'help' for more information."}, state} + end + + def handle_call({:swap, userA, userB}, _from, {schedule, users, idNameMap, nameIdMap, manager}) do + userA = userA + |> String.trim() + # |> String.to_atom() + + userB = userB + |> String.trim() + # |> String.to_atom() + + userIdA = Map.get(nameIdMap, userA) |> String.to_atom + userIdB = Map.get(nameIdMap, userB) |> String.to_atom + + case validate_user_ids([userIdA, userIdB], users) do + true -> + {nameA, dateA} = Keyword.get(schedule, userIdA) + {nameB, dateB} = Keyword.get(schedule, userIdB) + + newSchedule = schedule + |> Keyword.replace!(userIdA, {nameA, dateB}) + |> Keyword.replace!(userIdB, {nameB, dateA}) + + save_state(newSchedule, @schedule_file) + manager = get_manager_of_week(newSchedule) + {:reply, :ok, {newSchedule, users, idNameMap, nameIdMap, manager}} + + msg -> {:reply, {:error, msg}, {schedule, users, idNameMap, nameIdMap, manager}} + + end + end + + def handle_call(:remove_users, _from, {_, _, idNameMap, nameIdMap, _}) do + save_state([], @users_file) + save_state([], @schedule_file) + {:reply, :ok, {[], [], idNameMap, :no_specified}} + end + + def handle_call(:remove_schedule, _from, {_, users, idNameMap,nameIdMap, _}) do + save_state([], @schedule_file) + {:reply, :ok, {[], users, idNameMap,nameIdMap, :no_specified}} + end + + + ########### + # Private # + ########### + + defp save_state(data, file) do + content = :io_lib.format("~tp.~n", [data]) + :ok = :file.write_file(file, content) + end + + defp restore_state() do + usersData = :file.consult(@users_file) + scheduleData = :file.consult(@schedule_file) + + users = case usersData do + {:ok, [values]} -> values + _ -> [] + end + + schedule = case scheduleData do + {:ok, [values]} -> values + _ -> [] + end + + manager = get_manager_of_week(schedule) + + members = Slack.Web.Users.list(%{token: "xoxb-236968418545-LS34wziPDCf07sha2bkzhbe3"}) + idNameMap = members + |> Map.get("members") + |> Enum.map(fn(member) -> + {member["id"], member["name"]} + end) + + nameIdMap = members + |> Map.get("members") + |> Enum.map(fn(member) -> + {member["name"], member["id"]} + end) + + + {:ok, {schedule, users, Map.new(idNameMap), Map.new(nameIdMap), manager}} + end + + defp buildFullName(fullName, acc \\ "") + defp buildFullName([], acc) do + String.trim(acc) + end + defp buildFullName([h|rest], acc) do + acc = acc <> " " <> h + buildFullName rest, acc + end + + defp add_to_schedule([] = schedule, _user, _fullName), do: schedule + + defp add_to_schedule(schedule, user, fullName) do + {_, {_, lastDate}} = Enum.max_by(schedule, fn({_, {_, date}}) -> date end) + Keyword.put(schedule, user, {fullName, get_next_valid_date(Date.add(lastDate, 7))}) + end + + defp validate_user_ids([], _users), do: true + defp validate_user_ids([user| rest], users) do + case Keyword.has_key?(users, user) do + false -> "Invalid user id! The user #{user} is not a registered." + _ -> validate_user_ids(rest, users) + end + end + + defp build_schedule(list, startDate, schedule \\ []) + defp build_schedule([], _startDate, schedule), do: Enum.reverse(schedule) + + defp build_schedule([{k, v} | rest], startDate, schedule) do + startDate = get_next_valid_date(startDate) + schedule = Keyword.put_new(schedule, k, {v, startDate}) + nextDate = get_next_valid_date(Date.add(startDate, 7)) + build_schedule(rest,nextDate, schedule ) + end + + defp get_next_valid_date(date) do + holiday? = Enum.find(@holidays, fn(x) -> Date.compare(date, x) == :eq end) + case holiday? do + nil -> get_start_date(date) + _ -> get_next_valid_date(Date.add(date, 7)) + end + end + + defp get_manager_of_week([]), do: :no_specified + defp get_manager_of_week(schedule) do + startDate = get_start_date() + result = Enum.find(schedule, fn({_, {_, date}}) -> Date.compare(startDate, date) == :eq end) + case result do + nil -> :no_specified + {user,_} -> user + end + + end + + defp get_next_manager([]), do: :no_specified + defp get_next_manager(schedule) do + startDate = Date.add(get_start_date(), 7) + result = Enum.find(schedule, fn({_, {_, date}}) -> Date.compare(startDate, date) == :eq end) + case result do + nil -> :no_specified + {user,_} -> user + end + end + + defp get_start_date( date \\ Date.utc_today) do + case Date.day_of_week(date) do + 1 -> date + value -> Date.add(date, -(value-1)) + end + end + +end diff --git a/lib/scheduler/dishwasher_scheduler.ex b/lib/scheduler/dishwasher_scheduler.ex new file mode 100644 index 0000000..152e78d --- /dev/null +++ b/lib/scheduler/dishwasher_scheduler.ex @@ -0,0 +1,56 @@ +defmodule Scheduler.DishwasherScheduler do + @moduledoc false + + use Cronex.Scheduler + + require Logger + + every :monday, at: "07:00" do + Brain.DishwasherManager.set_manager_of_the_week() + end + + every :monday, at: "08:00" do + msg = "Good morning! Enjoy your week as dishwasher manager and don't forget your duties." + send_notification(msg) + end + + every :wednesday, at: "09:00" do + msg = "Good morning! Don't forget your dishwasher duties." + send_notification(msg) + end + + every :wednesday, at: "17:00" do + msg = "Hey Dishwasher Manager! I hope you did your duties today :slightly_smiling_face:" + send_notification(msg) + end + + every :friday, at: "09:00" do + msg = "Good morning! Don't forget your dishwasher duties." + send_notification(msg) + end + + every :friday, at: "17:00" do + msg = """ + Hi Dishwasher Manager! We hope you did all your dishwasher duties this week :stuck_out_tongue_winking_eye: + Have a nice weekend! + """ + send_notification(msg) + end + + every :wednesday, at: "10:00" do + msg = """ + Hello! Next week you will be our Dishwasher Manager :tada: + If you will be out next week please change your turn with another person using the `swap_with` command. + e.g. `swap_with @cdetroye` + """ + {:ok, manager} = Brain.DishwasherManager.next_manager() + SlackManager.send_private_message(msg, manager) + end + + defp send_notification(msg) do + {:ok, manager, _} = Brain.DishwasherManager.manager?() + SlackManager.send_private_message(msg, manager) + end + + +end diff --git a/lib/slack/slack_manager.ex b/lib/slack/slack_manager.ex index fc167db..d985051 100644 --- a/lib/slack/slack_manager.ex +++ b/lib/slack/slack_manager.ex @@ -57,6 +57,14 @@ defmodule SlackManager do {:noreply, state} end + @doc """ + Sends a message over Slack to the given user. + """ + def handle_cast({:send_private_msg, msg, user}, state) do + send(state.client, {:send_private_msg, msg, user}) + {:noreply, state} + end + ######### # Calls # ######### @@ -67,7 +75,11 @@ defmodule SlackManager do """ def handle_call({:dealias, m}, _from, state) do dealiased = ~r/\<@([a-zA-Z0-9]+)\>/ - |> Regex.replace(m, fn _, x -> dealias_userhash("#{x}", state) end) + |> Regex.replace(m, + fn _, x -> + {:ok, username} = dealias_userhash("#{x}", state) + username + end) {:reply, {:ok, dealiased}, state} end @@ -185,4 +197,8 @@ defmodule SlackManager do def send_message(m, channel) do GenServer.cast(SlackManager, {:send_msg, m, channel}) end + + def send_private_message(m, user) do + GenServer.cast(SlackManager, {:send_private_msg, m, user}) + end end diff --git a/lib/slack/slacklogic.ex b/lib/slack/slacklogic.ex index d93e4ea..a615db8 100644 --- a/lib/slack/slacklogic.ex +++ b/lib/slack/slacklogic.ex @@ -26,7 +26,7 @@ defmodule SlackLogic do which are in the form of some-sort of hashes. """ def handle_event(message = %{type: "message", text: text, user: from}, slack, state) do - Logger.debug ">> #{text}" + Logger.debug ">>> #{text}" {:ok, sender} = SlackManager.dealias_userhash(from) {:ok, m} = SlackManager.dealias_message(text) {:ok, {_, chan}} = SlackManager.dehash_channel(message.channel) @@ -70,6 +70,16 @@ defmodule SlackLogic do {:ok, state} end + @doc """ + Info's come from the outside. IT allows us to send messages to the Slack + process. + """ + def handle_info({:send_private_msg, text, user}, slack, state) do + Logger.debug "<< #{user}: #{text}" + send_message(text, "@#{user}", slack) + {:ok, state} + end + def handle_info(_,_, state) do {:ok, state} end diff --git a/lib/slackbot.ex b/lib/slackbot.ex index 5a59159..8f262f9 100644 --- a/lib/slackbot.ex +++ b/lib/slackbot.ex @@ -10,6 +10,7 @@ defmodule Slackbot do supervisor(Supervisor.Connection, []), # - The data of the bot (karma etc) supervisor(Supervisor.Brain, []), + supervisor(Supervisor.Scheduler, []), ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html diff --git a/lib/supervisor/bot.ex b/lib/supervisor/bot.ex index 0f34434..1686fc7 100644 --- a/lib/supervisor/bot.ex +++ b/lib/supervisor/bot.ex @@ -7,13 +7,14 @@ defmodule Supervisor.Bot do def init(opts) do children = [ - worker(Bot.Karma, [opts]), - worker(Bot.ChuckNorris, [opts]), - worker(Bot.Resto, [opts]), - worker(Bot.Cronjob, [opts]), - worker(Bot.Rss, [opts]), - worker(Bot.Benvolios, [opts]), - worker(Bot.Misc, [opts]) +# worker(Bot.Karma, [opts]), +# worker(Bot.ChuckNorris, [opts]), +# worker(Bot.Resto, [opts]), +# worker(Bot.Cronjob, [opts]), +# worker(Bot.Rss, [opts]), +# worker(Bot.Benvolios, [opts]), + worker(Bot.DiswasherManager, [opts]), +# worker(Bot.Misc, [opts]) ] supervise(children, strategy: :one_for_one) end diff --git a/lib/supervisor/brain.ex b/lib/supervisor/brain.ex index bc69476..65a5a3d 100644 --- a/lib/supervisor/brain.ex +++ b/lib/supervisor/brain.ex @@ -8,7 +8,8 @@ defmodule Supervisor.Brain do def init(_state) do children = [ worker(Brain.Karma, []), - worker(Brain.Benvolios, []) + worker(Brain.Benvolios, []), + worker(Brain.DishwasherManager, []) ] supervise children, strategy: :one_for_one end diff --git a/lib/supervisor/scheduler.ex b/lib/supervisor/scheduler.ex new file mode 100644 index 0000000..7213480 --- /dev/null +++ b/lib/supervisor/scheduler.ex @@ -0,0 +1,17 @@ +defmodule Supervisor.Scheduler do + @moduledoc false + + use Supervisor + + def start_link(state \\ []) do + {:ok, _pid} = Supervisor.start_link(__MODULE__, state, [{:name, __MODULE__}]) + end + + def init(_state) do + children = [ + worker(Scheduler.DishwasherScheduler, []) + ] + supervise children, strategy: :one_for_one + end + +end diff --git a/mix.exs b/mix.exs index 6a20a75..a2e0405 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Slackbot.Mixfile do def project do [app: Slackbot, version: "0.0.1", - elixir: "~> 1.4", + elixir: "~> 1.5", deps: deps()] end @@ -26,12 +26,13 @@ defmodule Slackbot.Mixfile do # # Type `mix help deps` for more examples and options defp deps do - [{:exgenius, "~> 0.0.2"}, - {:slack, "~> 0.12.0"}, - {:poison, "~> 3.0"}, - {:timex, "~> 3.1.7"}, - {:feeder_ex, "~> 1.0"}, - {:html_entities, "~> 0.3"} + [ + {:slack, "~> 0.13.0"}, + {:poison, "~> 3.1.0"}, + {:timex, "~> 3.2.1"}, + {:feeder_ex, "~> 1.1.0"}, + {:html_entities, "~> 0.4.0"}, + {:cronex, git: "https://github.com/rhumbertgz/cronex.git"} ] end end diff --git a/mix.lock b/mix.lock index 201238b..2358093 100644 --- a/mix.lock +++ b/mix.lock @@ -1,21 +1,19 @@ -%{"certifi": {:hex, :certifi, "1.2.1", "c3904f192bd5284e5b13f20db3ceac9626e14eeacfbb492e19583cf0e37b22be", [:rebar3], [], "hexpm"}, - "combine": {:hex, :combine, "0.9.6", "8d1034a127d4cbf6924c8a5010d3534d958085575fa4d9b878f200d79ac78335", [:mix], [], "hexpm"}, - "exgenius": {:hex, :exgenius, "0.0.5", "bf486cffa31c4add0b600193939438b9a4936540435880ba08066e0dafcf2226", [:mix], [{:exjsx, "~> 3.1", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 0.5", [hex: :httpoison, repo: "hexpm", optional: false]}], "hexpm"}, - "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, - "feeder": {:hex, :feeder, "2.2.1", "9b1236d32cf971a049968b4c3955fa4808bd132b347af6e30a06fc2093751796", [:make], [], "hexpm"}, - "feeder_ex": {:hex, :feeder_ex, "1.1.0", "0be3732255cdb45dec949e0ede6852b5261c9ff173360e8274a6ac65183b2b55", [:mix], [{:feeder, "~> 2.2", [hex: :feeder, repo: "hexpm", optional: false]}], "hexpm"}, - "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], [], "hexpm"}, - "hackney": {:hex, :hackney, "1.8.6", "21a725db3569b3fb11a6af17d5c5f654052ce9624219f1317e8639183de4a423", [:rebar3], [{:certifi, "1.2.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.0.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, - "html_entities": {:hex, :html_entities, "0.3.0", "2f278ffc69c3f0cbd5996628fef37cf922fa715f3e04b9f38924c8adced3f25a", [:mix], [], "hexpm"}, - "httpoison": {:hex, :httpoison, "0.12.0", "8fc3d791c5afe6beb0093680c667dd4ce712a49d89c38c3fe1a43100dd76cf90", [:mix], [{:hackney, "~> 1.8.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "5.0.2", "ac203208ada855d95dc591a764b6e87259cb0e2a364218f215ad662daa8cd6b4", [:rebar3], [{:unicode_util_compat, "0.2.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, - "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, - "slack": {:hex, :slack, "0.12.0", "a305f87adca043e056a86f92151d085a8c359718f9c4ca7f1d1012d67b75db8a", [:mix], [{:httpoison, "~> 0.11", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}, {:websocket_client, "~> 1.2.4", [hex: :websocket_client, repo: "hexpm", optional: false]}], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, - "timex": {:hex, :timex, "3.1.21", "7d1ec0f73c4668bea71f38af6357d75992f356a7e032c69620c5e87ca4b95c66", [:mix], [{:combine, "~> 0.7", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, - "tzdata": {:hex, :tzdata, "0.5.12", "1c17b68692c6ba5b6ab15db3d64cc8baa0f182043d5ae9d4b6d35d70af76f67b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.2.0", "dbbccf6781821b1c0701845eaf966c9b6d83d7c3bfc65ca2b78b88b8678bfa35", [:rebar3], [], "hexpm"}, - "websocket_client": {:hex, :websocket_client, "1.2.4", "14ec1ca4b6d247b44ccd9a80af8f6ca98328070f6c1d52a5cb00bc9d939d63b8", [:rebar3], [], "hexpm"}} +%{"certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], []}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], []}, + "cronex": {:git, "https://github.com/rhumbertgz/cronex.git", "5500b9f57975751ed0c997c0677f9fabb8fb0ab4", []}, + "feeder": {:hex, :feeder, "2.2.4", "56ec535cf2f79719bc53b5c2abe5f6cf481fc01e5ae6229ab7cc829644f039ec", [:make], []}, + "feeder_ex": {:hex, :feeder_ex, "1.1.0", "0be3732255cdb45dec949e0ede6852b5261c9ff173360e8274a6ac65183b2b55", [:mix], [{:feeder, "~> 2.2", [hex: :feeder, optional: false]}]}, + "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []}, + "hackney": {:hex, :hackney, "1.9.0", "51c506afc0a365868469dcfc79a9d0b94d896ec741cfd5bd338f49a5ec515bfe", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, optional: false]}, {:idna, "5.1.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, + "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], []}, + "httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, optional: false]}]}, + "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, optional: false]}]}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, + "slack": {:hex, :slack, "0.12.0", "a305f87adca043e056a86f92151d085a8c359718f9c4ca7f1d1012d67b75db8a", [:mix], [{:httpoison, "~> 0.11", [hex: :httpoison, optional: false]}, {:poison, "~> 3.0", [hex: :poison, optional: false]}, {:websocket_client, "~> 1.2.4", [hex: :websocket_client, optional: false]}]}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, + "timex": {:hex, :timex, "3.1.24", "d198ae9783ac807721cca0c5535384ebdf99da4976be8cefb9665a9262a1e9e3", [:mix], [{:combine, "~> 0.7", [hex: :combine, optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, optional: false]}]}, + "tzdata": {:hex, :tzdata, "0.5.12", "1c17b68692c6ba5b6ab15db3d64cc8baa0f182043d5ae9d4b6d35d70af76f67b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, optional: false]}]}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], []}, + "websocket_client": {:hex, :websocket_client, "1.2.4", "14ec1ca4b6d247b44ccd9a80af8f6ca98328070f6c1d52a5cb00bc9d939d63b8", [:rebar3], []}}