├── .gitattributes ├── config ├── test.exs ├── dev.exs ├── prod.exs ├── config.exs └── deploy.rb ├── apps ├── db │ ├── test │ │ ├── test_helper.exs │ │ └── connections_test.exs │ ├── priv │ │ └── repo │ │ │ └── migrations │ │ │ ├── .gitkeep │ │ │ ├── 20170927111932_create_chat.exs │ │ │ ├── 20170928090743_create_customers.exs │ │ │ └── 20180824124553_chat_id_modification.exs │ ├── lib │ │ ├── db │ │ │ ├── repo.ex │ │ │ ├── application.ex │ │ │ ├── chat.ex │ │ │ ├── customer.ex │ │ │ └── connections.ex │ │ └── tasks │ │ │ └── repo.ex │ ├── config │ │ ├── config.exs │ │ ├── test.exs │ │ ├── dev.exs │ │ └── prod.exs │ ├── .gitignore │ └── mix.exs └── buckler_bot │ ├── config │ ├── dev.exs │ ├── prod.exs │ ├── test.exs │ └── config.exs │ ├── test │ └── test_helper.exs │ ├── lib │ └── buckler_bot │ │ ├── gettext.ex │ │ ├── name_validator │ │ ├── validate_max_length.ex │ │ └── name_validator.ex │ │ ├── application.ex │ │ ├── bot.ex │ │ ├── captcha │ │ └── captcha.ex │ │ ├── i18n.ex │ │ ├── handlers │ │ └── private.ex │ │ └── chain.ex │ ├── README.md │ ├── priv │ └── gettext │ │ ├── en │ │ └── LC_MESSAGES │ │ │ └── default.po │ │ ├── default.pot │ │ └── ru │ │ └── LC_MESSAGES │ │ └── default.po │ └── mix.exs ├── rel ├── vm.args └── config.exs ├── img ├── logo.svg.png └── logo.svg ├── Gemfile ├── README.md ├── Gemfile.lock ├── .gitignore ├── mix.exs ├── buckler_bot.eye ├── .circleci └── config.yml └── mix.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sgv binary 2 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /apps/db/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/buckler_bot/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /apps/buckler_bot/config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /apps/buckler_bot/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /apps/db/priv/repo/migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | .gitkeep 2 | -------------------------------------------------------------------------------- /apps/buckler_bot/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, level: :debug 4 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, level: :warn 4 | -------------------------------------------------------------------------------- /rel/vm.args: -------------------------------------------------------------------------------- 1 | -name ${NODENAME}@127.0.0.1 2 | -setcookie ${NODENAME} 3 | -------------------------------------------------------------------------------- /img/logo.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BucklerBot/buckler/HEAD/img/logo.svg.png -------------------------------------------------------------------------------- /apps/db/lib/db/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule DB.Repo do 2 | use Ecto.Repo, otp_app: :db 3 | require Ecto.Query 4 | end 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'mina', git: 'https://github.com/tomato-fox/mina.git', require: false 4 | -------------------------------------------------------------------------------- /apps/buckler_bot/lib/buckler_bot/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule BucklerBot.Gettext do 2 | use Gettext, otp_app: :buckler_bot 3 | end 4 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | import_config "../apps/*/config/config.exs" 4 | import_config "#{Mix.env}.exs" 5 | -------------------------------------------------------------------------------- /apps/db/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :db, 4 | ecto_repos: [DB.Repo] 5 | 6 | import_config "#{Mix.env}.exs" 7 | -------------------------------------------------------------------------------- /apps/db/test/connections_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DB.ConnectionsTest do 2 | use ExUnit.Case 3 | alias DB.{Repo, Chat, Customer} 4 | 5 | test ":: create_chat" do 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Buckler [![CircleCI](https://circleci.com/gh/BucklerBot/buckler/tree/master.svg?style=shield)](https://circleci.com/gh/BucklerBot/buckler/tree/master) 2 | 3 | **TODO: Add description** 4 | 5 | -------------------------------------------------------------------------------- /apps/db/lib/tasks/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule DB.Tasks.Repo do 2 | def migrate do 3 | path = Path.join(:code.priv_dir(:db), "repo/migrations") 4 | Ecto.Migrator.run(DB.Repo, path, :up, all: true) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /apps/db/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :db, DB.Repo, 4 | adapter: Ecto.Adapters.Postgres, 5 | database: "buckler_test", 6 | username: "postgres", 7 | password: "postgres", 8 | hostname: "localhost", 9 | port: "5432", 10 | loggers: [] 11 | 12 | config :logger, 13 | level: :info 14 | -------------------------------------------------------------------------------- /apps/db/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :db, DB.Repo, 4 | adapter: Ecto.Adapters.Postgres, 5 | database: "buckler_development", 6 | username: "postgres", 7 | password: "postgres", 8 | hostname: "localhost", 9 | port: "5432", 10 | loggers: [] 11 | 12 | config :logger, 13 | level: :info 14 | -------------------------------------------------------------------------------- /apps/buckler_bot/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :buckler_bot, :telegram, 4 | token: System.get_env("TELEGRAM_TOKEN") 5 | 6 | config :buckler_bot, 7 | loyalty_count: 3 8 | 9 | config :buckler_bot, BucklerBot.Repo, 10 | db_name: System.get_env("DB_NAME") 11 | 12 | import_config "#{Mix.env}.exs" 13 | -------------------------------------------------------------------------------- /apps/buckler_bot/lib/buckler_bot/name_validator/validate_max_length.ex: -------------------------------------------------------------------------------- 1 | defmodule BucklerBot.NameValidator.ValidateMaxLength do 2 | def validate(%{first_name: first_name}, max_length) do 3 | case String.length(first_name) > max_length do 4 | true -> {:error, __MODULE__} 5 | false -> {:ok, __MODULE__} 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /apps/buckler_bot/lib/buckler_bot/application.ex: -------------------------------------------------------------------------------- 1 | defmodule BucklerBot.Application do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | import Supervisor.Spec, warn: false 6 | 7 | children = [ 8 | BucklerBot.Bot, 9 | DB.Repo 10 | ] 11 | 12 | Supervisor.start_link(children, strategy: :one_for_one) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /apps/db/config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :db, DB.Repo, 4 | adapter: Ecto.Adapters.Postgres, 5 | database: "buckler", 6 | username: System.get_env("BUCKLER_SQL_USER"), 7 | password: System.get_env("BUCKLER_PASS"), 8 | hostname: System.get_env("BUCKLER_HOST"), 9 | port: "5432", 10 | loggers: [] 11 | 12 | config :logger, level: :info 13 | -------------------------------------------------------------------------------- /apps/db/lib/db/application.ex: -------------------------------------------------------------------------------- 1 | defmodule DB.Application do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | import Supervisor.Spec, warn: false 6 | 7 | children = [ 8 | supervisor(DB.Repo, []) 9 | ] 10 | 11 | opts = [strategy: :one_for_one, name: DB.Supervisor] 12 | Supervisor.start_link(children, opts) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /apps/buckler_bot/lib/buckler_bot/bot.ex: -------------------------------------------------------------------------------- 1 | defmodule BucklerBot.Bot do 2 | use Agala.Bot.Poller, [ 3 | otp_app: :buckler_bot, 4 | provider: Agala.Provider.Telegram, 5 | chain: BucklerBot.Chain, 6 | provider_params: %Agala.Provider.Telegram.Conn.ProviderParams{ 7 | poll_timeout: :infinity, 8 | token: Application.get_env(:buckler_bot, :telegram)[:token] 9 | } 10 | ] 11 | end 12 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/tomato-fox/mina.git 3 | revision: 1211c0ff1c302d6c6ef6ef5b3821080798289480 4 | specs: 5 | mina (1.0.7) 6 | open4 (~> 1.3.4) 7 | rake 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | open4 (1.3.4) 13 | rake (12.0.0) 14 | 15 | PLATFORMS 16 | ruby 17 | 18 | DEPENDENCIES 19 | mina! 20 | 21 | BUNDLED WITH 22 | 1.15.1 23 | -------------------------------------------------------------------------------- /apps/db/priv/repo/migrations/20170927111932_create_chat.exs: -------------------------------------------------------------------------------- 1 | defmodule DB.Repo.Migrations.CreateChat do 2 | #lagnuage codes https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes 3 | use Ecto.Migration 4 | 5 | def change do 6 | create table "chats", primary_key: false do 7 | add :id, :bigint, primary_key: true 8 | add :lang, :string, default: "en" 9 | add :attempts, :integer, default: 3 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /apps/buckler_bot/lib/buckler_bot/name_validator/name_validator.ex: -------------------------------------------------------------------------------- 1 | defmodule BucklerBot.NameValidator do 2 | @pipeline [ 3 | {BucklerBot.NameValidator.ValidateMaxLength, 50} 4 | ] 5 | 6 | def validate(user) do 7 | Enum.reduce(@pipeline, {:ok, __MODULE__}, fn 8 | {module, args}, {:ok, _} = acc -> 9 | case module.validate(user, args) do 10 | {:ok, _} -> acc 11 | {:error, validator} -> {:error, validator} 12 | end 13 | 14 | _, {:error, _} = acc -> 15 | acc 16 | end) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /apps/buckler_bot/lib/buckler_bot/captcha/captcha.ex: -------------------------------------------------------------------------------- 1 | defmodule BucklerBot.Captcha do 2 | alias BucklerBot.Captcha 3 | import BucklerBot.Gettext 4 | 5 | defstruct [ 6 | captcha: nil, 7 | answer: nil 8 | ] 9 | 10 | def generate_captcha(lang) do 11 | lh = :rand.uniform(100) 12 | rh = :rand.uniform(100) 13 | 14 | Elixir.Gettext.with_locale BucklerBot.Gettext, lang, fn -> 15 | %Captcha{ 16 | captcha: gettext("*Calculate*: *%{lh}+%{rh}=...*", lh: lh, rh: rh), 17 | answer: "#{lh+rh}" 18 | } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /apps/db/.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 | -------------------------------------------------------------------------------- /apps/db/lib/db/chat.ex: -------------------------------------------------------------------------------- 1 | defmodule DB.Chat do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @cast_fields [:id, :lang, :attempts] 6 | @required_fields [:id] 7 | 8 | @primary_key {:id, :string, autogenerate: false} 9 | schema "chats" do 10 | field :lang, :string, default: "en" 11 | field :attempts, :integer, default: 3 12 | 13 | has_many :customers, DB.Customer 14 | end 15 | 16 | def changeset(struct, params \\ %{}) do 17 | struct 18 | |> cast(params, @cast_fields) 19 | |> validate_required(@required_fields) 20 | |> unique_constraint(:id, name: :chats_pkey) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /.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 | 22 | # Binaries 23 | bin 24 | 25 | # Elixir LS 26 | .elixir_ls -------------------------------------------------------------------------------- /apps/buckler_bot/lib/buckler_bot/i18n.ex: -------------------------------------------------------------------------------- 1 | defmodule BucklerBot.I18n do 2 | import BucklerBot.Gettext 3 | require Logger 4 | 5 | def welcome_message(lang, name, captcha, attempts) do 6 | Logger.debug("Welcome message with locale: #{lang}") 7 | Elixir.Gettext.with_locale BucklerBot.Gettext, lang, fn -> 8 | gettext( 9 | """ 10 | Hello, *%{name}*! 11 | 12 | Please, solve the captcha: 13 | 14 | %{captcha} 15 | 16 | Attempts remaining: *%{attempts}* 17 | If you don't answer - you'll get banned from the channel... 18 | Good luck! 19 | """, 20 | name: name, 21 | captcha: captcha, 22 | attempts: attempts 23 | ) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /apps/db/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DB.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :db, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.4", 13 | # build_embedded: Mix.env == :prod, 14 | # start_permanent: Mix.env == :prod, 15 | deps: deps() 16 | ] 17 | end 18 | 19 | def application do 20 | [ 21 | # extra_applications: [ 22 | # :logger 23 | # ], 24 | # mod: {DB.Application, []} 25 | ] 26 | end 27 | 28 | defp deps do 29 | [ 30 | {:postgrex, ">= 0.0.0"}, 31 | {:ecto, "~> 2.1"}, 32 | {:ex_doc, "~> 0.18"} 33 | ] 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /apps/buckler_bot/README.md: -------------------------------------------------------------------------------- 1 | # BucklerBot 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `buckler_bot` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [{:buckler_bot, "~> 0.1.0"}] 13 | end 14 | ``` 15 | 16 | ## Run 17 | 18 | As console: 19 | 20 | ```elixir 21 | TELEGRAM_TOKEN=TOKEN DB_NAME=prodb iex -S mix 22 | ``` 23 | 24 | ## Tests 25 | 26 | ```elixir 27 | TELEGRAM_TOKEN=TOKEN DB_NAME=testdb mix test 28 | ``` 29 | 30 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 31 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 32 | be found at [https://hexdocs.pm/buckler_bot](https://hexdocs.pm/buckler_bot). 33 | -------------------------------------------------------------------------------- /apps/db/lib/db/customer.ex: -------------------------------------------------------------------------------- 1 | defmodule DB.Customer do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @cast_fields [ 6 | :user_id, :name, :connected_message_id, :answer, 7 | :welcome_message_id, :lang, :attempts, 8 | ] 9 | @required_fields [ 10 | :user_id, :name, :connected_message_id, :answer, 11 | :lang, :attempts, 12 | ] 13 | schema "customers" do 14 | field :name, :string 15 | field :user_id, :integer 16 | field :connected_message_id, :integer 17 | field :welcome_message_id, :integer 18 | field :answer, :string 19 | field :lang, :string 20 | field :attempts, :integer 21 | 22 | belongs_to :chat, DB.Chat, type: :string 23 | end 24 | 25 | def changeset(struct, params \\ %{}) do 26 | struct 27 | |> cast(params, @cast_fields) 28 | |> validate_required(@required_fields) 29 | |> assoc_constraint(:chat) 30 | |> unique_constraint(:user_id, name: :customers_chat_id_user_id_index) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Buckler.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [apps_path: "apps", 6 | build_embedded: Mix.env == :prod, 7 | start_permanent: Mix.env == :prod, 8 | deps: deps(), 9 | aliases: aliases() 10 | ] 11 | end 12 | 13 | # Dependencies can be Hex packages: 14 | # 15 | # {:my_dep, "~> 0.3.0"} 16 | # 17 | # Or git/path repositories: 18 | # 19 | # {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 20 | # 21 | # Type "mix help deps" for more examples and options. 22 | # 23 | # Dependencies listed here are available only for this project 24 | # and cannot be accessed from applications inside the apps folder 25 | defp deps do 26 | [ 27 | {:distillery, "~> 1.3", runtime: false}, 28 | ] 29 | end 30 | 31 | defp aliases do 32 | [ 33 | "release.update": [ 34 | "deps.get", 35 | "compile", 36 | "release" 37 | ] 38 | ] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /apps/db/priv/repo/migrations/20170928090743_create_customers.exs: -------------------------------------------------------------------------------- 1 | defmodule DB.Repo.Migrations.CreateCustomer do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table "customers" do 6 | add :name, :string, comment: "User's name" 7 | add :user_id, :bigint, comment: "User's Telegram ID" 8 | add :answer, :string, comment: "User's captcha answer" 9 | add :chat_id, references("chats", type: :bigint, on_delete: :delete_all, on_update: :update_all), comment: "Chat's Telegram ID for entered user" 10 | add :connected_message_id, :integer, comment: "'%user% connected to channel' message's ID" 11 | add :welcome_message_id, :integer, comment: "Bot's reply for user's connection" 12 | 13 | add :lang, :string, comment: "Language for bot to speak with user" 14 | add :attempts, :integer, comment: "Number of current retries for user - with 0 here he's banned" 15 | end 16 | create index("customers", [:chat_id, :user_id], unique: true) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /buckler_bot.eye: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'timeout' 3 | 4 | USER = 'deploy'.freeze 5 | BUCKLER_APP_DIR = '/home/deploy/buckler_bot/current'.freeze 6 | 7 | Eye.app :buckler_bot do 8 | stop_on_delete true 9 | trigger :flapping, times: 10, within: 1.minute 10 | 11 | group 'elixir_release' do 12 | chain action: :restart, grace: 30.seconds 13 | 14 | stdall '/tmp/trash.log' 15 | 16 | %w[8081].each do |port| 17 | process "buckler_bot-#{port}" do 18 | pid_file "tmp/buckler_bot_#{port}.pid" 19 | daemonize true 20 | 21 | start_command "/bin/su - #{USER} -c \"cd #{BUCKLER_APP_DIR}/rel/buckler_bot/bin && PORT=#{port} REPLACE_OS_VARS=true NODENAME=master_node_#{port} ./buckler foreground\"" 22 | 23 | stop_signals [:QUIT, 3.seconds, :KILL] 24 | 25 | restart_command "/bin/su - #{USER} -c \"cd #{BUCKLER_APP_DIR}/rel/buckler_bot/bin && PORT=#{port} REPLACE_OS_VARS=true NODENAME=master_node_#{port} ./buckler stop\"" 26 | 27 | restart_grace 10.seconds 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /apps/buckler_bot/priv/gettext/en/LC_MESSAGES/default.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | #: lib/buckler_bot/captcha/captcha.ex:16 14 | msgid "*Calculate*: *%{lh}+%{rh}=...*" 15 | msgstr "" 16 | 17 | #: lib/buckler_bot/handlers/private.ex:21 18 | msgid "Hello, %{first_name}!\n\nI'm *BucklerBot*, and I'll defend your group or chat from dirsty spammers.\n\n1. Add me as Administrator to your group.\n2. Give me only two rights: to *Ban users* and to *Delete messages*\n3. *???*\n4. Enjoy!\n\nIf you have any questions or issues - welcome to our project's\nrepo - https://github.com/BucklerBot/buckler\n" 19 | msgstr "" 20 | 21 | #: lib/buckler_bot/i18n.ex:8 22 | msgid "Hello, *%{name}*!\n\nPlease, solve the captcha:\n\n%{captcha}\n\nAttempts remaining: *%{attempts}*\nIf you don't answer - you'll get banned from the channel...\nGood luck!\n" 23 | msgstr "" 24 | -------------------------------------------------------------------------------- /apps/buckler_bot/priv/gettext/default.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | msgid "" 11 | msgstr "" 12 | 13 | #: lib/buckler_bot/captcha/captcha.ex:16 14 | msgid "*Calculate*: *%{lh}+%{rh}=...*" 15 | msgstr "" 16 | 17 | #: lib/buckler_bot/handlers/private.ex:21 18 | msgid "Hello, %{first_name}!\n\nI'm *BucklerBot*, and I'll defend your group or chat from dirsty spammers.\n\n1. Add me as Administrator to your group.\n2. Give me only two rights: to *Ban users* and to *Delete messages*\n3. *???*\n4. Enjoy!\n\nIf you have any questions or issues - welcome to our project's\nrepo - https://github.com/BucklerBot/buckler\n" 19 | msgstr "" 20 | 21 | #: lib/buckler_bot/i18n.ex:8 22 | msgid "Hello, *%{name}*!\n\nPlease, solve the captcha:\n\n%{captcha}\n\nAttempts remaining: *%{attempts}*\nIf you don't answer - you'll get banned from the channel...\nGood luck!\n" 23 | msgstr "" 24 | -------------------------------------------------------------------------------- /apps/db/priv/repo/migrations/20180824124553_chat_id_modification.exs: -------------------------------------------------------------------------------- 1 | defmodule DB.Repo.Migrations.ChatIdModification do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop table("customers") 6 | drop table("chats") 7 | 8 | create table "chats", primary_key: false do 9 | add :id, :string, primary_key: true 10 | add :lang, :string, default: "en" 11 | add :attempts, :integer, default: 3 12 | end 13 | 14 | create table "customers" do 15 | add :name, :string, comment: "User's name" 16 | add :user_id, :bigint, comment: "User's Telegram ID" 17 | add :answer, :string, comment: "User's captcha answer" 18 | add :chat_id, references("chats", type: :string, on_delete: :delete_all, on_update: :update_all), comment: "Chat's Telegram ID for entered user" 19 | add :connected_message_id, :integer, comment: "'%user% connected to channel' message's ID" 20 | add :welcome_message_id, :integer, comment: "Bot's reply for user's connection" 21 | 22 | add :lang, :string, comment: "Language for bot to speak with user" 23 | add :attempts, :integer, comment: "Number of current retries for user - with 0 here he's banned" 24 | end 25 | create index("customers", [:chat_id, :user_id], unique: true) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /apps/buckler_bot/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BucklerBot.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :buckler_bot, 6 | compilers: [:gettext] ++ Mix.compilers, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.4", 13 | build_embedded: Mix.env == :prod, 14 | start_permanent: Mix.env == :prod, 15 | deps: deps()] 16 | end 17 | 18 | # Configuration for the OTP application 19 | # 20 | # Type "mix help compile.app" for more information 21 | def application do 22 | # Specify extra applications you'll use from Erlang/Elixir 23 | [extra_applications: [:logger], 24 | mod: {BucklerBot.Application, []}] 25 | end 26 | 27 | # Dependencies can be Hex packages: 28 | # 29 | # {:my_dep, "~> 0.3.0"} 30 | # 31 | # Or git/path repositories: 32 | # 33 | # {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 34 | # 35 | # To depend on another app inside the umbrella: 36 | # 37 | # {:my_app, in_umbrella: true} 38 | # 39 | # Type "mix help deps" for more examples and options 40 | defp deps do 41 | [ 42 | {:db, in_umbrella: true}, 43 | {:agala_telegram, "~> 3.0"}, 44 | {:gettext, "~> 0.13.1"} 45 | ] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/buckler 5 | docker: 6 | - image: noma4i/docker-elixir:latest 7 | - image: postgres:9.4.1 8 | environment: 9 | POSTGRES_USER: postgres 10 | steps: 11 | - checkout 12 | - type: cache-restore 13 | key: gemfile-{{ checksum "Gemfile.lock" }} 14 | - type: shell 15 | name: Install Bundler 16 | command: gem install bundler 17 | - type: shell 18 | name: Install Ruby Dependencies 19 | command: bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3 20 | - type: cache-save 21 | key: gemfile-{{ checksum "Gemfile.lock" }} 22 | paths: 23 | - vendor/bundle 24 | - run: mix local.hex --force 25 | - run: mix local.rebar --force 26 | - run: MIX_ENV=test mix do deps.get, compile 27 | - run: 28 | working_directory: ~/buckler/apps/buckler_bot 29 | name: Test 30 | environment: 31 | TELEGRAM_TOKEN: 123 32 | DB_NAME: testdb 33 | command: mix test 34 | - deploy: 35 | name: Deploy Master to Server 36 | command: | 37 | if [ "${CIRCLE_BRANCH}" == "master" ]; then 38 | DEPLOY_TARGET=146.185.181.86 bundle exec mina deploy 39 | fi 40 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | require 'mina/git' 4 | require 'mina/deploy' 5 | 6 | set :codename, 'buckler' 7 | 8 | set :term_mode, :pretty 9 | 10 | set :user, 'deploy' 11 | set :domain, ENV['DEPLOY_TARGET'] 12 | set :deploy_to, '/home/deploy/buckler_bot' 13 | set :repository, 'git@github.com:BucklerBot/buckler.git' 14 | set :branch, 'master' 15 | set :shared_dirs, %w[elixir_logs] 16 | set :forward_agent, true 17 | set :execution_mode, :system 18 | set :ssh_options, '-o StrictHostKeyChecking=no' 19 | set :identity_file, '/root/.ssh/id_rsa' 20 | 21 | task setup: :environment do 22 | command %(mkdir -p "#{fetch(:deploy_to)}/shared/elixir_logs") 23 | command %(mkdir -p "#{fetch(:deploy_to)}/shared/deps") 24 | command %(mkdir -p "#{fetch(:deploy_to)}/shared/_build") 25 | command %(mix local.hex --force) 26 | command %(mix local.rebar --force) 27 | end 28 | 29 | desc 'Deploys production' 30 | task :production do 31 | command %(export MIX_ENV=prod) 32 | end 33 | 34 | task deploy: :environment do 35 | deploy do 36 | invoke :'git:clone' 37 | invoke :'deploy:link_shared_paths' 38 | command %(MIX_ENV=prod mix release.update) 39 | on :launch do 40 | in_path(fetch(:current_path)) do 41 | command %(cp ./buckler_bot.eye /home/deploy/) 42 | command %(sudo service eye restart) 43 | command %(sudo eye restart buckler_bot) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /apps/buckler_bot/lib/buckler_bot/handlers/private.ex: -------------------------------------------------------------------------------- 1 | defmodule BucklerBot.Handlers.Private do 2 | use Agala.Provider.Telegram, :handler 3 | 4 | import BucklerBot.Gettext 5 | require Logger 6 | 7 | def init(opts), do: opts 8 | def call(conn = %Agala.Conn{ 9 | request: %{"message" => %{"chat" => %{"id" => chat_id, "type" => "private"}, "text" => "/ping"}} 10 | }, _) do 11 | conn 12 | |> send_message(chat_id, "BucklerBot is now working") 13 | conn 14 | end 15 | 16 | def call(conn = %Agala.Conn{ 17 | request: %{"message" => %{"chat" => %{"id" => chat_id, "first_name" => first_name, "type" => "private"}, "text" => "/start"}} 18 | }, _) do 19 | conn 20 | |> send_message( 21 | chat_id, 22 | gettext( 23 | """ 24 | Hello, %{first_name}! 25 | 26 | I'm *BucklerBot*, and I'll defend your group or chat from dirsty spammers. 27 | 28 | 1. Add me as Administrator to your group. 29 | 2. Give me only two rights: to *Ban users* and to *Delete messages* 30 | 3. *???* 31 | 4. Enjoy! 32 | 33 | If you have any questions or issues - welcome to our project's 34 | repo - https://github.com/BucklerBot/buckler 35 | """, 36 | first_name: first_name), 37 | parse_mode: "Markdown" 38 | ) 39 | conn 40 | end 41 | 42 | ################################################# 43 | 44 | def call(conn = %Agala.Conn{ 45 | request: request 46 | }, _) do 47 | Logger.error("Unexpected message:\n#{inspect request}") 48 | conn 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /apps/buckler_bot/priv/gettext/ru/LC_MESSAGES/default.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: ru\n" 12 | 13 | #: lib/buckler_bot/captcha/captcha.ex:16 14 | msgid "*Calculate*: *%{lh}+%{rh}=...*" 15 | msgstr "*Вычисли*: *%{lh}+%{rh}=...*" 16 | 17 | #: lib/buckler_bot/handlers/private.ex:21 18 | msgid "Hello, %{first_name}!\n\nI'm *BucklerBot*, and I'll defend your group or chat from dirsty spammers.\n\n1. Add me as Administrator to your group.\n2. Give me only two rights: to *Ban users* and to *Delete messages*\n3. *???*\n4. Enjoy!\n\nIf you have any questions or issues - welcome to our project's\nrepo - https://github.com/BucklerBot/buckler\n" 19 | msgstr "Привет, %{first_name}!\n\nЯ - *BucklerBot*, и я буду защищать твою группу от грязных спаммеров.\n\n1. Добавь меня в свою группу в качестве *Администратора*.\n2. Дай мне только два права: *Банить пользователей* и *Удалять сообщения*\n3. *???*\n4. Наслаждайся!\n\nЕсли есть вопросы или предложения - приходи в \nрепозиторий - https://github.com/BucklerBot/buckler\n" 20 | 21 | #: lib/buckler_bot/i18n.ex:8 22 | msgid "Hello, *%{name}*!\n\nPlease, solve the captcha:\n\n%{captcha}\n\nAttempts remaining: *%{attempts}*\nIf you don't answer - you'll get banned from the channel...\nGood luck!\n" 23 | msgstr "Привет, *%{name}*!\n\nПожалуйста, реши капчу:\n\n%{captcha}\n\nОсталось попыток: *%{attempts}*\nЕсли не решишь - забаню навеки...\nУдачи!\n" 24 | -------------------------------------------------------------------------------- /rel/config.exs: -------------------------------------------------------------------------------- 1 | # Import all plugins from `rel/plugins` 2 | # They can then be used by adding `plugin MyPlugin` to 3 | # either an environment, or release definition, where 4 | # `MyPlugin` is the name of the plugin module. 5 | Path.join(["rel", "plugins", "*.exs"]) 6 | |> Path.wildcard() 7 | |> Enum.map(&Code.eval_file(&1)) 8 | 9 | use Mix.Releases.Config, 10 | # This sets the default release built by `mix release` 11 | default_release: :default, 12 | # This sets the default environment used by `mix release` 13 | default_environment: Mix.env() 14 | 15 | # For a full list of config options for both releases 16 | # and environments, visit https://hexdocs.pm/distillery/configuration.html 17 | 18 | 19 | # You may define one or more environments in this file, 20 | # an environment's settings will override those of a release 21 | # when building in that environment, this combination of release 22 | # and environment configuration is called a profile 23 | 24 | environment :dev do 25 | # If you are running Phoenix, you should make sure that 26 | # server: true is set and the code reloader is disabled, 27 | # even in dev mode. 28 | # It is recommended that you build with MIX_ENV=prod and pass 29 | # the --env flag to Distillery explicitly if you want to use 30 | # dev mode. 31 | set dev_mode: true 32 | set include_erts: false 33 | set cookie: :"OOjxZrybU>O}8z<^fsx2jlQ}^RgV|XXXcaJtRsKPkei2*,ot82WC{LZ4xjxkAmR%pBgui=]Q%f/Y`5(wdj" 41 | set output_dir: "rel/buckler_bot" 42 | end 43 | 44 | # You may define one or more releases in this file. 45 | # If you have not set a default release, or selected one 46 | # when running `mix release`, the first release in the file 47 | # will be used by default 48 | 49 | release :buckler do 50 | set version: "0.1.0" 51 | set applications: [ 52 | :runtime_tools, 53 | buckler_bot: :permanent 54 | ] 55 | end 56 | 57 | -------------------------------------------------------------------------------- /apps/db/lib/db/connections.ex: -------------------------------------------------------------------------------- 1 | defmodule DB.Connections do 2 | import Ecto.Query 3 | alias DB.{Customer, Chat, Repo} 4 | require Logger 5 | 6 | defp chatuser_query(chat_id, user_id) do 7 | from c in Customer, 8 | where: c.chat_id == ^chat_id, 9 | where: c.user_id == ^user_id, 10 | select: c 11 | end 12 | 13 | def get_chatuser(chat_id, user_id) do 14 | case Repo.one(chatuser_query(chat_id, user_id)) do 15 | nil -> {:error, :not_found} 16 | user -> {:ok, user} 17 | end 18 | end 19 | def get_or_create_chat(chat_id) do 20 | case Repo.get(Chat, chat_id) do 21 | nil -> create_chat(%{id: chat_id}) 22 | chat -> {:ok, chat} 23 | end 24 | end 25 | 26 | def create_chat(params) do 27 | %Chat{} 28 | |> Chat.changeset(params) 29 | |> Repo.insert() 30 | end 31 | 32 | def create_chatuser(chat, user_id, name, answer, connected_message_id) do 33 | chat 34 | |> Ecto.build_assoc(:customers, %{ 35 | name: name, 36 | connected_message_id: connected_message_id, 37 | user_id: user_id, 38 | answer: answer, 39 | lang: chat.lang, 40 | attempts: chat.attempts 41 | }) 42 | |> Repo.insert() 43 | end 44 | 45 | def connect_user(chat_id, user_id, name, answer, connected_message_id) do 46 | with {:ok, chat} <- get_or_create_chat(chat_id) do 47 | create_chatuser(chat, user_id, name, answer, connected_message_id) 48 | else 49 | _ -> 50 | case Repo.transaction(fn -> 51 | with {:ok, chat} <- create_chat(%{chat_id: chat_id}) do 52 | create_chatuser(chat, user_id, name, answer, connected_message_id) 53 | end 54 | end) do 55 | {:ok, created} -> created 56 | err -> err 57 | end 58 | end 59 | end 60 | 61 | def decrease_attempts(chat_id, user_id, answer) do 62 | with {:ok, customer} <- get_chatuser(chat_id, user_id) do 63 | Repo.update(Customer.changeset(customer, %{answer: answer, attempts: customer.attempts-1})) 64 | end 65 | end 66 | 67 | def delete_chatuser(chat_id, user_id) do 68 | with {:ok, customer} <- get_chatuser(chat_id, user_id) do 69 | Repo.delete(customer) 70 | else 71 | _ -> Logger.error("Delete user not found") 72 | end 73 | end 74 | 75 | def user_unauthorized?(chat_id, user_id) do 76 | case get_chatuser(chat_id, user_id) do 77 | {:error, _ } -> false 78 | {:ok, customer} -> {true, customer} 79 | end 80 | end 81 | 82 | @doc """ 83 | This function will update id for welcome message from buckler bot stored in the database. 84 | """ 85 | def update_welcome_message(chat_id, user_id, message_id) do 86 | with {:ok, customer} <- get_chatuser(chat_id, user_id) do 87 | Repo.update(Customer.changeset(customer, %{welcome_message_id: message_id})) 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "agala": {:hex, :agala, "3.0.0", "00eba9262858c65b835cc9eebdf488137c70d9a26d1473349663b1a8a930d218", [:mix], [], "hexpm"}, 3 | "agala_telegram": {:hex, :agala_telegram, "3.0.0", "43195cd75f6f350ebdb60a16542080dd531827f6f4b3d3f3deb18f4114c6486e", [:mix], [{:agala, "~> 3.0", [hex: :agala, repo: "hexpm", optional: false]}, {:construct, "~> 1.0", [hex: :construct, repo: "hexpm", optional: false]}, {:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.2", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, 6 | "construct": {:hex, :construct, "1.1.0", "095273c4f383590efa653bbe3d44766e68d35a0617602c58e62ab537df283fff", [:mix], [], "hexpm"}, 7 | "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 8 | "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, 9 | "distillery": {:hex, :distillery, "1.5.3", "b2f4fc34ec71ab4f1202a796f9290e068883b042319aa8c9aa45377ecac8597a", [:mix], [], "hexpm"}, 10 | "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, 11 | "ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 12 | "ex_doc": {:hex, :ex_doc, "0.18.4", "4406b8891cecf1352f49975c6d554e62e4341ceb41b9338949077b0d4a97b949", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], [], "hexpm"}, 14 | "hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.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"}, 15 | "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 16 | "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 17 | "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 18 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 19 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 20 | "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, 21 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, 22 | "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, 23 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, 24 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, 25 | } 26 | -------------------------------------------------------------------------------- /apps/buckler_bot/lib/buckler_bot/chain.ex: -------------------------------------------------------------------------------- 1 | defmodule BucklerBot.Chain do 2 | use Agala.Chain.Builder 3 | use Agala.Provider.Telegram, :handler 4 | 5 | chain(Agala.Chain.Loopback) 6 | chain(:handle) 7 | 8 | alias DB.Connections 9 | alias BucklerBot.I18n 10 | require Logger 11 | 12 | # Forward private messages into specific chain 13 | def handle( 14 | conn = %Agala.Conn{ 15 | request: %{"message" => %{"chat" => %{"type" => "private"}}} 16 | }, 17 | _ 18 | ) do 19 | Logger.debug("New request in private chat!") 20 | BucklerBot.Handlers.Private.call(conn, []) 21 | end 22 | 23 | # done 24 | def handle( 25 | conn = %Agala.Conn{ 26 | request: %{ 27 | "message" => %{ 28 | "chat" => %{"id" => chat_id}, 29 | "left_chat_member" => %{"id" => user_id}, 30 | "message_id" => leave_message_id 31 | } 32 | } 33 | }, 34 | _ 35 | ) do 36 | chat_id = "#{chat_id}" 37 | 38 | case Connections.user_unauthorized?(chat_id, user_id) do 39 | {true, user} -> 40 | Connections.delete_chatuser(chat_id, user_id) 41 | delete_message(conn, chat_id, user.connected_message_id) 42 | delete_message(conn, chat_id, user.welcome_message_id) 43 | delete_message(conn, chat_id, leave_message_id) 44 | 45 | _ -> 46 | :do_nothing 47 | end 48 | 49 | conn |> Agala.Conn.halt() 50 | end 51 | 52 | # done 53 | def handle( 54 | conn = %Agala.Conn{ 55 | request: %{ 56 | "message" => %{ 57 | "message_id" => message_id, 58 | "chat" => %{ 59 | "id" => chat_id 60 | }, 61 | "new_chat_member" => %{ 62 | "first_name" => first_name, 63 | "id" => user_id, 64 | "is_bot" => false 65 | } 66 | } 67 | } 68 | }, 69 | _ 70 | ) do 71 | Logger.debug("New user connected: #{first_name}") 72 | chat_id = "#{chat_id}" 73 | 74 | ### Firstly we check if this user is not valid by our validator 75 | case BucklerBot.NameValidator.validate(%{first_name: first_name}) do 76 | {:ok, _} -> 77 | with {:ok, chat} <- Connections.get_or_create_chat(chat_id), 78 | %{captcha: captcha, answer: answer} <- 79 | BucklerBot.Captcha.generate_captcha(chat.lang), 80 | {:error, :not_found} <- Connections.get_chatuser(chat_id, user_id), 81 | {:ok, user} <- 82 | Connections.connect_user(chat_id, user_id, first_name, answer, message_id) do 83 | send_message( 84 | conn, 85 | chat_id, 86 | I18n.welcome_message(user.lang, user.name, captcha, user.attempts), 87 | reply_to_message_id: message_id, 88 | parse_mode: "Markdown" 89 | ) 90 | |> handle_welcome_message(user_id) 91 | end 92 | 93 | {:error, _} -> 94 | kick_chat_member(conn, chat_id, user_id) 95 | delete_message(conn, chat_id, message_id) 96 | end 97 | 98 | conn 99 | end 100 | 101 | defp handle_welcome_message( 102 | {:ok, 103 | %{ 104 | "ok" => true, 105 | "result" => %{ 106 | "chat" => %{ 107 | "id" => chat_id 108 | }, 109 | "message_id" => message_id, 110 | "text" => _ 111 | } 112 | }}, 113 | user_id 114 | ) do 115 | chat_id = "#{chat_id}" 116 | DB.Connections.update_welcome_message(chat_id, user_id, message_id) 117 | end 118 | 119 | ### Dealing with incoming messages 120 | def handle( 121 | conn = %Agala.Conn{ 122 | request: %{ 123 | "message" => %{ 124 | "text" => text, 125 | "message_id" => message_id, 126 | "chat" => %{ 127 | "id" => chat_id 128 | }, 129 | "from" => %{ 130 | "id" => user_id 131 | } 132 | } 133 | } 134 | }, 135 | _ 136 | ) do 137 | chat_id = "#{chat_id}" 138 | 139 | case Connections.user_unauthorized?(chat_id, user_id) do 140 | {true, user} -> process_captcha_check(user.answer == text, conn, message_id, user) 141 | _ -> :user_authorized_do_nothing 142 | end 143 | 144 | conn |> Agala.Conn.halt() 145 | end 146 | 147 | ### The same with media message 148 | def handle( 149 | conn = %Agala.Conn{ 150 | request: %{ 151 | "message" => %{ 152 | "message_id" => message_id, 153 | "chat" => %{ 154 | "id" => chat_id 155 | }, 156 | "from" => %{ 157 | "id" => user_id 158 | } 159 | } 160 | } 161 | }, 162 | _ 163 | ) do 164 | Logger.debug("New media message!") 165 | chat_id = "#{chat_id}" 166 | 167 | case Connections.user_unauthorized?(chat_id, user_id) do 168 | {true, user} -> process_captcha_check(false, conn, message_id, user) 169 | _ -> :user_authorized_do_nothing 170 | end 171 | 172 | conn |> Agala.Conn.halt() 173 | end 174 | 175 | # OK case 176 | defp process_captcha_check(true, conn, message_id, user) do 177 | delete_message(conn, user.chat_id, user.welcome_message_id) 178 | delete_message(conn, user.chat_id, message_id) 179 | Connections.delete_chatuser(user.chat_id, user.user_id) 180 | end 181 | 182 | # Fail case 183 | defp process_captcha_check( 184 | false, 185 | conn, 186 | message_id, 187 | %{attempts: attempts} = user 188 | ) 189 | when attempts < 2 do 190 | # ban here 191 | {:ok, user} = Connections.delete_chatuser(user.chat_id, user.user_id) 192 | delete_message(conn, user.chat_id, user.welcome_message_id) 193 | delete_message(conn, user.chat_id, user.connected_message_id) 194 | delete_message(conn, user.chat_id, message_id) 195 | kick_chat_member(conn, user.chat_id, user.user_id) 196 | end 197 | 198 | defp process_captcha_check(false, conn, message_id, user) do 199 | # decrease attempt 200 | with %{captcha: captcha, answer: answer} <- BucklerBot.Captcha.generate_captcha(user.lang), 201 | {:ok, user} <- Connections.decrease_attempts(user.chat_id, user.user_id, answer) do 202 | delete_message(conn, user.chat_id, user.welcome_message_id) 203 | delete_message(conn, user.chat_id, message_id) 204 | 205 | send_message( 206 | conn, 207 | user.chat_id, 208 | I18n.welcome_message(user.lang, user.name, captcha, user.attempts), 209 | reply_to_message_id: user.connected_message_id, 210 | parse_mode: "Markdown" 211 | ) 212 | |> handle_welcome_message(user.user_id) 213 | end 214 | end 215 | 216 | ################################################# 217 | 218 | def handle( 219 | conn = %Agala.Conn{ 220 | request: request 221 | }, 222 | _ 223 | ) do 224 | Logger.error("Unexpected message:\n#{inspect(request)}") 225 | conn 226 | end 227 | end 228 | -------------------------------------------------------------------------------- /img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 31 | 35 | 39 | 40 | 48 | 52 | 56 | 57 | 67 | 68 | 87 | 89 | 90 | 92 | image/svg+xml 93 | 95 | 96 | 97 | 98 | 99 | 104 | 109 | 114 | 119 | 124 | 129 | 134 | 139 | 144 | 151 | 158 | 165 | 172 | 175 | 181 | 186 | 191 | 196 | 197 | 202 | 203 | 204 | --------------------------------------------------------------------------------