├── config ├── dev.exs ├── prod.exs ├── test.exs └── config.exs ├── test ├── test_helper.exs └── app_test.exs ├── app.yaml ├── lib ├── app │ ├── matcher.ex │ ├── commands │ │ └── outside.ex │ ├── poller.ex │ ├── commands.ex │ ├── tools.ex │ ├── commander.ex │ └── router.ex └── app.ex ├── .gitignore ├── README.md ├── mix.exs ├── LICENSE.md ├── mix.lock └── jokes.json /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | env: flex 2 | runtime: gs://elixir-runtime/elixir.yaml 3 | runtime_config: 4 | release_app: lambdinha -------------------------------------------------------------------------------- /test/app_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AppTest do 2 | use ExUnit.Case 3 | doctest App 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :app, 4 | bot_name: "" 5 | 6 | config :nadia, 7 | token: "" 8 | 9 | import_config "#{Mix.env}.exs" 10 | -------------------------------------------------------------------------------- /lib/app/matcher.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Matcher do 2 | use GenServer 3 | alias App.Commands 4 | 5 | # Server 6 | 7 | def start_link do 8 | GenServer.start_link __MODULE__, :ok, name: __MODULE__ 9 | end 10 | 11 | def init(:ok) do 12 | {:ok, 0} 13 | end 14 | 15 | def handle_cast(message, state) do 16 | Commands.match_message message 17 | 18 | {:noreply, state} 19 | end 20 | 21 | # Client 22 | 23 | def match(message) do 24 | GenServer.cast __MODULE__, message 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /lib/app/commands/outside.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Commands.Outside do 2 | # Notice that here we just `use` Commander. Router is only 3 | # used to map commands to actions. It's best to keep routing 4 | # only in App.Commands file. Commander gives us helpful 5 | # macros to deal with Nadia functions. 6 | use App.Commander 7 | 8 | # Functions must have as first parameter a variable named 9 | # update. Otherwise, macros (like `send_message`) will not 10 | # work as expected. 11 | def outside(update) do 12 | Logger.log :info, "Command /outside" 13 | 14 | send_message "This came from a separate module." 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/app.ex: -------------------------------------------------------------------------------- 1 | defmodule App do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | bot_name = Application.get_env(:app, :bot_name) 6 | 7 | unless String.valid?(bot_name) do 8 | IO.warn """ 9 | Env not found Application.get_env(:app, :bot_name) 10 | This will give issues when generating commands 11 | """ 12 | end 13 | 14 | if bot_name == "" do 15 | IO.warn "An empty bot_name env will make '/anycommand@' valid" 16 | end 17 | 18 | import Supervisor.Spec, warn: false 19 | 20 | children = [ 21 | worker(App.Poller, []), 22 | worker(App.Matcher, []) 23 | ] 24 | 25 | opts = [strategy: :one_for_one, name: App.Supervisor] 26 | Supervisor.start_link children, opts 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lambdinha-bot 2 | O bot do Telegram do Lambda Study Group. 3 | 4 | ## Comandos: 5 | - `/help`: Cita os comandos do bot 6 | - `/welcome`: Manda uma mensagem de boas vindas 7 | - `/ranking`: Mostra o ranking dos desafios do Lambda 8 | - `/desafios`: Mostra a lista de desafios do Lambda 9 | - `/joke`: Conta uma piada sobre programação funcional 10 | - `/xkcd`: Envia uma tirinha do xkcd 11 | - `/monads`: Define monads de maneira intuitiva 12 | 13 | Para as funções `/joke`, `/desafios` e `/xkcd`, você pode usar um argumento para definir uma saída não aleatória. Por exemplo, para enviar a xkcd número 200: 14 | 15 | `/xkcd 200` 16 | 17 | Além disso, o comando `/ranking` suporta um argumento que diz quantos usuários deve mostrar. 18 | 19 | 20 | ## Contribuindo: 21 | Sinta-se livre para mandar um pull request! :) 22 | Sugestões de comandos para implementação nos issues. 23 | 24 | ## Licença: 25 | MIT. 26 | 27 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :app, 6 | version: "0.1.0", 7 | elixir: "~> 1.3", 8 | default_task: "server", 9 | build_embedded: Mix.env == :prod, 10 | start_permanent: Mix.env == :prod, 11 | deps: deps(), 12 | releases: [ 13 | lambdinha: [ 14 | include_executables_for: [:unix], 15 | applications: [runtime_tools: :permanent] 16 | ] 17 | ], 18 | aliases: aliases()] 19 | end 20 | 21 | def application do 22 | [applications: [:logger, :nadia, :httpoison], 23 | mod: {App, []}] 24 | end 25 | 26 | defp deps do 27 | [ 28 | {:nadia, "0.4.4"}, 29 | {:poison, "~> 3.1"}, 30 | {:httpoison, "~>1.1.1"}, 31 | {:hackney, github: "benoitc/hackney", override: true} 32 | ] 33 | end 34 | 35 | defp aliases do 36 | [server: "run --no-halt"] 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Lubien 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/app/poller.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Poller do 2 | use GenServer 3 | require Logger 4 | 5 | # Server 6 | 7 | def start_link do 8 | Logger.log :info, "Started poller" 9 | GenServer.start_link __MODULE__, :ok, name: __MODULE__ 10 | end 11 | 12 | def init(:ok) do 13 | update() 14 | {:ok, 0} 15 | end 16 | 17 | def handle_cast(:update, offset) do 18 | new_offset = Nadia.get_updates([offset: offset]) 19 | |> process_messages 20 | 21 | {:noreply, new_offset + 1, 100} 22 | end 23 | 24 | def handle_info(:timeout, offset) do 25 | update() 26 | {:noreply, offset} 27 | end 28 | 29 | # Client 30 | 31 | def update do 32 | GenServer.cast __MODULE__, :update 33 | end 34 | 35 | # Helpers 36 | 37 | defp process_messages({:ok, []}), do: -1 38 | defp process_messages({:ok, results}) do 39 | results 40 | |> Enum.map(fn %{update_id: id} = message -> 41 | message 42 | |> process_message 43 | 44 | id 45 | end) 46 | |> List.last 47 | end 48 | defp process_messages({:error, %Nadia.Model.Error{reason: reason}}) do 49 | Logger.log :error, "Reason: #{Kernel.inspect(reason)}" 50 | 51 | -1 52 | end 53 | defp process_messages({:error, error}) do 54 | Logger.log :error, error 55 | 56 | -1 57 | end 58 | defp process_message(nil), do: IO.inspect "nil" 59 | defp process_message(message) do 60 | try do 61 | App.Matcher.match message 62 | rescue 63 | err in MatchError -> 64 | Logger.log :warn, "Errored with #{err} at #{Poison.encode! message}" 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "hackney": {:git, "https://github.com/benoitc/hackney.git", "5b7363c14071abe92d4039a7fc96371bd6eee91e", []}, 4 | "httpoison": {:hex, :httpoison, "1.1.1", "96ed7ab79f78a31081bb523eefec205fd2900a02cda6dbc2300e7a1226219566", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 7 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, 8 | "nadia": {:hex, :nadia, "0.4.4", "fc4ba813e0c4aafc75936f8c318a7a18683694be51783ef7bf365214c4717cd6", [:mix], [{:httpoison, "~> 1.1.1", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 3.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, 10 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 11 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, 12 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, 13 | } 14 | -------------------------------------------------------------------------------- /jokes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "joke": "P: qual a bebida preferida dos programadores funcionais? R: Li Monada" 4 | }, 5 | { 6 | "joke": "Eu ia fazer café com Map mas ai pensei 'poxa, melhor fazer com Filtro'" 7 | }, 8 | { 9 | "joke": "I'd make a pun about Haskell evalution, but that would be just Lazy" 10 | }, 11 | { 12 | "joke": "Hackers invadiram os servidores da NASA e roubaram código sensível escritos em Lisp. Chantagearam a empresa por dinheiro para apagar esse código. Para provar que estavam de posse desse código enviaram as últimas duas páginas impressas: ')))))))))))))))))))))))))…'" 13 | }, 14 | { 15 | "joke": "Functional programmer: (noun) One who names variables 'x', names functions 'f', and names code patterns 'zygohistomorphic prepromorphism'." 16 | }, 17 | { 18 | "joke": "qual é o vingador mais funcional? o funcTHOR" 19 | }, 20 | { 21 | "joke": "funcionou, é funcional" 22 | }, 23 | { 24 | "joke": "Hey girl, were you a monad? cause now that you're gone i'm feeling the side effects" 25 | }, 26 | { 27 | "joke": "O que o programador Haskell pediu no restaurante chinês? Bifumctor" 28 | }, 29 | { 30 | "joke": "O que comunistas e programadores funcionais tem em comum? Eles odeiam classes." 31 | }, 32 | { 33 | "joke": "P: Quais os beneficios de estudar tail recursion? R: Estudar tail recursion." 34 | }, 35 | { 36 | "joke": "Once you stop doing functional programming...You never return." 37 | }, 38 | { 39 | "joke": "Put Haskell on your resume even if you don't know it. When asked, say your resume is lazy and you'll learn it when results are needed." 40 | }, 41 | { 42 | "joke": "https://qphl.fs.quoracdn.net/main-qimg-c27ab68426981d432144ca27a4d99988.webp" 43 | }, 44 | { 45 | "joke": "The problem with Haskell is that it is a language built on lazy evaluation and nobody actually called for it" 46 | }, 47 | { 48 | "joke": "https://imgs.xkcd.com/comics/haskell.png" 49 | } 50 | 51 | 52 | ] 53 | -------------------------------------------------------------------------------- /lib/app/commands.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Commands do 2 | use App.Router 3 | use App.Commander 4 | use App.Tools 5 | 6 | alias App.Commands.Outside 7 | 8 | command "welcome" do 9 | Logger.log :info, "Command /welcome" 10 | send_message "Seja bem vindo ao Lambda Study Group! Leia as regras do grupo no pinado e visite nosso GitHub(https://github.com/lambda-study-group) ou nosso site(https://lambda-study-group.github.io). Digite /ajuda para ver mais comandos!" 11 | end 12 | 13 | command "monads" do 14 | Logger.log :info, "Command /monads" 15 | send_message "Monads are just monoids in the category of endofunctors." 16 | end 17 | 18 | command ["desafios", "desafio", "challenges", "challenge"] do 19 | Logger.log :info, "Command /desafios | /desafio | /challenges | /challenge" 20 | App.Tools.get_args(update.message.text) 21 | |> App.Tools.get_challenges 22 | |> send_message 23 | end 24 | 25 | command "ranking" do 26 | Logger.log :info, "Command /ranking" 27 | App.Tools.get_args(update.message.text) 28 | |> App.Tools.get_ranking 29 | |> send_message 30 | end 31 | 32 | command "joke" do 33 | Logger.log :info, "Command /joke" 34 | App.Tools.get_args(update.message.text) 35 | |> App.Tools.get_joke 36 | |> send_message 37 | end 38 | 39 | command "ajuda" do 40 | Logger.log :info, "Command /ajuda" 41 | send_message "Lista de comandos: /welcome, /monads, /ranking, /desafios, /ajuda, /joke, /xkcd /kick" 42 | end 43 | 44 | command "xkcd" do 45 | Logger.log :info, "Command /xkcd" 46 | App.Tools.get_args(update.message.text) 47 | |> App.Tools.get_xkcd 48 | |> send_message 49 | end 50 | 51 | command "kick" do 52 | 53 | # Validate if Chat Type 'private', create error kick not available. 54 | validate = 55 | if update.message.chat.type == "private" do 56 | Logger.log :error, "Command cannot be run in private chats" 57 | {:error, message: "Cannot run /kick in private chats. Who you kinkin'?"} 58 | else 59 | # Get Bot details to get Bot id 60 | {:ok, %Nadia.Model.User{id: my_id}} = get_me() 61 | # Get Chat Member details for this chat to get Bot permissions 62 | {:ok, %Nadia.Model.ChatMember{status: my_status}} = get_chat_member(my_id) 63 | 64 | # Get Chat Member details to get User Permissions and check for admin / creator 65 | {:ok, %Nadia.Model.ChatMember{status: user_status}} = get_chat_member(update.message.from.id) 66 | 67 | # If insufficient permission, send error to Chat Group. 68 | if !(Enum.member? ["administrator", "creator"], user_status) do 69 | Logger.log :error, "User does not have enough permissions to kick another user" 70 | {:error, message: "You do not have enough permission to /kick a user"} 71 | else 72 | if !(Enum.member? ["administrator", "creator"], my_status) do 73 | Logger.log :error, "Bot does not have enough permissions to kick a user" 74 | {:error, message: "I do not have enough permission to /kick a user"} 75 | else 76 | {:ok} 77 | end 78 | end 79 | end 80 | 81 | case validate do 82 | {:error, message: message} -> 83 | send_message message 84 | {:ok} -> 85 | Enum.map( 86 | App.Tools.get_mentioned_users(update.message.entities), 87 | fn user_id -> 88 | case (kick_chat_member user_id) do 89 | {:error, error} -> 90 | Logger.log :error, error.reason 91 | send_message "An error occurred while kicking the user\n" <> error.reason 92 | _ -> 93 | Logger.log :info, "Kicked User" 94 | end 95 | end 96 | ) 97 | end 98 | end 99 | 100 | # just avoiding errors when no command is found 101 | message do 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/app/tools.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Tools do 2 | require Logger 3 | 4 | defmacro __using__(_opts) do 5 | quote do 6 | 7 | end 8 | end 9 | 10 | @doc """ 11 | returns array of args as 12 | "/command 1 2 3" -> "1 2 3" 13 | """ 14 | def get_args(text) do 15 | [_c | args] = String.split(text, " ") 16 | Enum.join(args, " ") 17 | end 18 | 19 | @doc """ 20 | ## Description 21 | Takes entities in message body as argument, 22 | and returns a list of user_id that have been mentioned in message 23 | ## Example 24 | /kick @User_1 @User_2 -> 184564595 284564595 25 | """ 26 | def get_mentioned_users(entities) do 27 | Enum.filter( 28 | entities, 29 | fn (entity) -> 30 | entity.type == "text_mention" 31 | end 32 | ) 33 | |> Enum.map(fn (entity) -> entity.user.id end) 34 | end 35 | 36 | # returns only the first digits as an integer 37 | def digit_to_int(digit) do 38 | {number, _rest} = Integer.parse(digit) 39 | number 40 | end 41 | 42 | def handle_response({:ok, %{status_code: 200, body: body}}) do 43 | {:ok, Poison.Parser.parse!(body)} 44 | end 45 | 46 | def handle_response({:ok, %{status_code: _, body: _body}}) do 47 | {:ok, "Error"} 48 | end 49 | 50 | 51 | # Challenge extraction 52 | 53 | def extract_challenges(map, _number = "") do 54 | {:ok, body} = map 55 | Enum.reduce body, [], fn (item, res) -> 56 | res ++ ['#{item["name"]} - soluções: #{item["solutions"]} - #{item["link"]} \n'] 57 | end 58 | end 59 | 60 | def extract_challenges(map, number) do 61 | {:ok, body} = map 62 | item = Enum.at body, digit_to_int number 63 | '#{item["name"]} - soluções: #{item["solutions"]} - #{item["link"]} \n' 64 | end 65 | 66 | # Ranking extraction 67 | 68 | def extract_ranking(map, _top = "") do 69 | {:ok, body} = map 70 | Enum.reduce body, [], fn (item, res) -> 71 | res ++ ['#{item["ranking"]} - #{item["user"]} - #{item["pontuation"]} \n'] 72 | end 73 | end 74 | 75 | def extract_ranking(map, top) do 76 | {:ok, body} = map 77 | Enum.reduce Enum.slice(body, 0, digit_to_int top), [], fn (item, res) -> 78 | res ++ ['#{item["ranking"]} - #{item["user"]} - #{item["pontuation"]} \n'] 79 | end 80 | end 81 | 82 | 83 | # XKCD extraction 84 | 85 | def extract_xkcd(map) do 86 | {:ok, body} = map 87 | body["img"] 88 | end 89 | 90 | def random_xkcd_number(map) do 91 | {:ok, body} = map 92 | n = body["num"] 93 | Enum.random(1..n) 94 | end 95 | 96 | 97 | # Joke extraction 98 | 99 | def extract_joke(map, _number = "") do 100 | {:ok, body} = map 101 | item = Enum.random body 102 | '#{item["joke"]}' 103 | end 104 | 105 | def extract_joke(map, number) do 106 | {:ok, body} = map 107 | item = Enum.at(body, digit_to_int number) 108 | '#{item["joke"]}' 109 | end 110 | 111 | 112 | # Command functions 113 | 114 | def get_ranking(top \\ "") do 115 | HTTPoison.get("https://raw.githubusercontent.com/lambda-study-group/desafios/master/ranking.json") 116 | |> handle_response 117 | |> extract_ranking(top) 118 | end 119 | 120 | def get_challenges(number \\ "") do 121 | HTTPoison.get("https://raw.githubusercontent.com/lambda-study-group/desafios/master/challenges.json") 122 | |> handle_response 123 | |> extract_challenges(number) 124 | end 125 | 126 | def get_joke(number \\ "") do 127 | HTTPoison.get("https://raw.githubusercontent.com/lambda-study-group/lambdinha-bot/master/jokes.json") 128 | |> handle_response 129 | |> extract_joke(number) 130 | end 131 | 132 | def get_xkcd(_number = "") do 133 | HTTPoison.get("https://xkcd.com/info.0.json") 134 | |> handle_response 135 | |> random_xkcd_number 136 | |> get_xkcd 137 | end 138 | 139 | def get_xkcd(number) do 140 | HTTPoison.get("https://xkcd.com/#{number}/info.0.json") 141 | |> handle_response 142 | |> extract_xkcd 143 | end 144 | 145 | end 146 | -------------------------------------------------------------------------------- /lib/app/commander.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Commander do 2 | @bot_name Application.get_env(:app, :bot_name) 3 | 4 | # Code injectors 5 | 6 | defmacro __using__(_opts) do 7 | quote do 8 | require Logger 9 | import App.Commander 10 | alias Nadia.Model 11 | alias Nadia.Model.InlineQueryResult 12 | end 13 | end 14 | 15 | # Sender Macros 16 | defmacro get_me() do 17 | quote do 18 | Nadia.get_me 19 | end 20 | end 21 | 22 | defmacro answer_callback_query(options \\ []) do 23 | quote bind_quoted: [options: options] do 24 | Nadia.answer_callback_query var!(update).callback_query.id, options 25 | end 26 | end 27 | 28 | defmacro answer_inline_query(results, options \\ []) do 29 | quote bind_quoted: [results: results, options: options] do 30 | Nadia.answer_inline_query var!(update).inline_query.id, results, options 31 | end 32 | end 33 | 34 | defmacro send_audio(audio, options \\ []) do 35 | quote bind_quoted: [audio: audio, options: options] do 36 | Nadia.send_audio get_chat_id(), audio, options 37 | end 38 | end 39 | 40 | defmacro send_chat_action(action) do 41 | quote bind_quoted: [action: action] do 42 | Nadia.send_chat_action get_chat_id(), action 43 | end 44 | end 45 | 46 | defmacro send_contact(phone_number, first_name, options \\ []) do 47 | quote bind_quoted: [phone_number: phone_number, first_name: first_name, 48 | options: options] do 49 | Nadia.send_contact get_chat_id(), phone_number, first_name, options 50 | end 51 | end 52 | 53 | defmacro send_document(document, options \\ []) do 54 | quote bind_quoted: [document: document, options: options] do 55 | Nadia.send_document get_chat_id(), document, options 56 | end 57 | end 58 | 59 | defmacro send_location(latitude, longitude, options \\ []) do 60 | quote bind_quoted: [latitude: latitude, longitude: longitude, 61 | options: options] do 62 | Nadia.send_location get_chat_id(), latitude, longitude, options 63 | end 64 | end 65 | 66 | defmacro send_message(text, options \\ []) do 67 | quote bind_quoted: [text: text, options: options] do 68 | Nadia.send_message get_chat_id(), text, options 69 | end 70 | end 71 | 72 | defmacro send_photo(photo, options \\ []) do 73 | quote bind_quoted: [photo: photo, options: options] do 74 | Nadia.send_photo get_chat_id(), photo, options 75 | end 76 | end 77 | 78 | defmacro send_sticker(sticker, options \\ []) do 79 | quote bind_quoted: [sticker: sticker, options: options] do 80 | Nadia.send_sticker get_chat_id(), sticker, options 81 | end 82 | end 83 | 84 | defmacro send_venue(latitude, longitude, title, address, options \\ []) do 85 | quote bind_quoted: [latitude: latitude, longitude: longitude, 86 | title: title, address: address, options: options] do 87 | Nadia.send_venue get_chat_id(), latitude, longitude, title, address, options 88 | end 89 | end 90 | 91 | defmacro send_video(video, options \\ []) do 92 | quote bind_quoted: [video: video, options: options] do 93 | Nadia.send_video get_chat_id(), video, options 94 | end 95 | end 96 | 97 | defmacro send_voice(voice, options \\ []) do 98 | quote bind_quoted: [voice: voice, options: options] do 99 | Nadia.send_voice get_chat_id(), voice, options 100 | end 101 | end 102 | 103 | # Action Macros 104 | 105 | defmacro forward_message(chat_id) do 106 | quote bind_quoted: [chat_id: chat_id] do 107 | Nadia.forward_message chat_id, get_chat_id(), var!(update).message.message_id 108 | end 109 | end 110 | 111 | defmacro get_chat do 112 | quote do 113 | Nadia.get_chat get_chat_id() 114 | end 115 | end 116 | 117 | defmacro get_chat_administrators do 118 | quote do 119 | Nadia.get_chat_administrators get_chat_id() 120 | end 121 | end 122 | 123 | defmacro get_chat_member(user_id) do 124 | quote bind_quoted: [user_id: user_id] do 125 | Nadia.get_chat_member get_chat_id(), user_id 126 | end 127 | end 128 | 129 | defmacro get_chat_members_count do 130 | quote do 131 | Nadia.get_chat_members_count get_chat_id() 132 | end 133 | end 134 | 135 | defmacro kick_chat_member(user_id) do 136 | quote bind_quoted: [user_id: user_id] do 137 | Nadia.kick_chat_member get_chat_id(), user_id 138 | end 139 | end 140 | 141 | defmacro leave_chat do 142 | quote do 143 | Nadia.leave_chat get_chat_id() 144 | end 145 | end 146 | 147 | defmacro unban_chat_member(user_id) do 148 | quote bind_quoted: [user_id: user_id] do 149 | Nadia.unban_chat_member get_chat_id(), user_id 150 | end 151 | end 152 | 153 | # Helpers 154 | 155 | defmacro get_chat_id do 156 | quote do 157 | case var!(update) do 158 | %{inline_query: inline_query} when not is_nil(inline_query) -> 159 | inline_query.from.id 160 | %{callback_query: callback_query} when not is_nil(callback_query) -> 161 | callback_query.message.chat.id 162 | %{message: %{chat: %{id: id}}} when not is_nil(id) -> 163 | id 164 | %{edited_message: %{chat: %{id: id}}} when not is_nil(id) -> 165 | id 166 | %{channel_post: %{chat: %{id: id}}} when not is_nil(id) -> 167 | id 168 | _ -> raise "No chat id found!" 169 | end 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /lib/app/router.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Router do 2 | @bot_name Application.get_env(:app, :bot_name) 3 | 4 | # Code injectors 5 | 6 | defmacro __using__(_opts) do 7 | quote do 8 | require Logger 9 | import App.Router 10 | 11 | def match_message(message) do 12 | try do 13 | apply __MODULE__, :do_match_message, [message] 14 | rescue 15 | err in FunctionClauseError -> 16 | Logger.log :warn, """ 17 | Errored when matching command. #{err} 18 | Message was: #{Poison.encode! message} 19 | """ 20 | end 21 | end 22 | end 23 | end 24 | 25 | def generate_message_matcher(handler) do 26 | quote do 27 | def do_match_message(var!(update)) do 28 | handle_message unquote(handler), [var!(update)] 29 | end 30 | end 31 | end 32 | 33 | defp generate_command(command, handler) do 34 | quote do 35 | def do_match_message(%{ 36 | message: %{ 37 | text: "/" <> unquote(command) 38 | } 39 | } = var!(update)) do 40 | handle_message unquote(handler), [var!(update)] 41 | end 42 | 43 | def do_match_message(%{ 44 | message: %{ 45 | text: "/" <> unquote(command) <> " " <> _ 46 | } 47 | } = var!(update)) do 48 | handle_message unquote(handler), [var!(update)] 49 | end 50 | 51 | def do_match_message(%{ 52 | message: %{ 53 | text: "/" <> unquote(command) <> "@" <> unquote(@bot_name) 54 | } 55 | } = var!(update)) do 56 | handle_message unquote(handler), [var!(update)] 57 | end 58 | 59 | def do_match_message(%{ 60 | message: %{ 61 | text: "/" <> unquote(command) <> "@" <> unquote(@bot_name) <> " " <> _ 62 | } 63 | } = var!(update)) do 64 | handle_message unquote(handler), [var!(update)] 65 | end 66 | end 67 | end 68 | 69 | def generate_inline_query_matcher(handler) do 70 | quote do 71 | def do_match_message(%{inline_query: inline_query} = var!(update)) 72 | when not is_nil(inline_query) do 73 | handle_message unquote(handler), [var!(update)] 74 | end 75 | end 76 | end 77 | 78 | def generate_inline_query_command(command, handler) do 79 | quote do 80 | def do_match_message(%{ 81 | inline_query: %{query: "/" <> unquote(command)} 82 | } = var!(update)) do 83 | handle_message unquote(handler), [var!(update)] 84 | end 85 | 86 | def do_match_message(%{ 87 | inline_query: %{query: "/" <> unquote(command) <> " " <> _} 88 | } = var!(update)) do 89 | handle_message unquote(handler), [var!(update)] 90 | end 91 | end 92 | end 93 | 94 | def generate_callback_query_matcher(handler) do 95 | quote do 96 | def do_match_message(%{callback_query: callback_query} = var!(update)) 97 | when not is_nil(callback_query) do 98 | handle_message unquote(handler), [var!(update)] 99 | end 100 | end 101 | end 102 | 103 | def generate_callback_query_command(command, handler) do 104 | quote do 105 | def do_match_message(%{ 106 | callback_query: %{data: "/" <> unquote(command)} 107 | } = var!(update)) do 108 | handle_message unquote(handler), [var!(update)] 109 | end 110 | 111 | def do_match_message(%{ 112 | callback_query: %{data: "/" <> unquote(command) <> " " <> _} 113 | } = var!(update)) do 114 | handle_message unquote(handler), [var!(update)] 115 | end 116 | end 117 | end 118 | 119 | # Receiver Macros 120 | 121 | ## Match All 122 | 123 | defmacro message(do: function) do 124 | generate_message_matcher(function) 125 | end 126 | 127 | defmacro message(module, function) do 128 | generate_message_matcher {module, function} 129 | end 130 | 131 | ## Command 132 | 133 | defmacro command(commands, do: function) 134 | when is_list(commands) do 135 | Enum.map commands, fn command -> 136 | generate_command(command, function) 137 | end 138 | end 139 | defmacro command(command, do: function) do 140 | generate_command(command, function) 141 | end 142 | 143 | defmacro command(commands, module, function) 144 | when is_list(commands) do 145 | Enum.map commands, fn command -> 146 | generate_command(command, {module, function}) 147 | end 148 | end 149 | defmacro command(command, module, function) do 150 | generate_command(command, {module, function}) 151 | end 152 | 153 | ## Inline query 154 | 155 | defmacro inline_query(do: function) do 156 | generate_inline_query_matcher(function) 157 | end 158 | 159 | defmacro inline_query(module, function) do 160 | generate_inline_query_matcher({module, function}) 161 | end 162 | 163 | defmacro inline_query_command(commands, do: function) 164 | when is_list(commands) do 165 | Enum.map commands, fn item -> 166 | generate_inline_query_command(item, function) 167 | end 168 | end 169 | defmacro inline_query_command(command, do: function) do 170 | generate_inline_query_command(command, function) 171 | end 172 | 173 | defmacro inline_query_command(commands, module, function) 174 | when is_list(commands) do 175 | Enum.map commands, fn item -> 176 | generate_inline_query_command(item, {module, function}) 177 | end 178 | end 179 | defmacro inline_query_command(command, module, function) do 180 | generate_inline_query_command(command, {module, function}) 181 | end 182 | 183 | ## Callback query 184 | 185 | defmacro callback_query(do: function) do 186 | generate_callback_query_matcher(function) 187 | end 188 | 189 | defmacro callback_query(module, function) do 190 | generate_callback_query_matcher({module, function}) 191 | end 192 | 193 | defmacro callback_query_command(commands, do: function) 194 | when is_list(commands) do 195 | Enum.map commands, fn item -> 196 | generate_callback_query_command(item, function) 197 | end 198 | end 199 | defmacro callback_query_command(command, do: function) do 200 | generate_callback_query_command(command, function) 201 | end 202 | 203 | defmacro callback_query_command(commands, module, function) 204 | when is_list(commands) do 205 | Enum.map commands, fn item -> 206 | generate_callback_query_command(item, {module, function}) 207 | end 208 | end 209 | defmacro callback_query_command(command, module, function) do 210 | generate_callback_query_command(command, {module, function}) 211 | end 212 | 213 | # Helpers 214 | 215 | def handle_message({module, function}, update) 216 | when is_atom(function) and is_list(update) do 217 | Task.start fn -> 218 | apply module, function, [hd update] 219 | end 220 | end 221 | def handle_message({module, function}, update) 222 | when is_atom(function) do 223 | Task.start fn -> 224 | apply module, function, [update] 225 | end 226 | end 227 | 228 | def handle_message(function, update) 229 | when is_function(function) do 230 | Task.start fn -> 231 | function.() 232 | end 233 | end 234 | 235 | def handle_message(_, _), do: nil 236 | end 237 | --------------------------------------------------------------------------------