Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
Send texts via twilio and store in a cache during development
  • Loading branch information
oestrich committed Aug 26, 2021
0 parents commit 3f102dc
Show file tree
Hide file tree
Showing 15 changed files with 424 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
stein_sms-*.tar

7 changes: 7 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2021 SmartLogic

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SteinSms

**TODO: Add description**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `stein_sms` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:stein_sms, "~> 0.1.0"}
]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/stein_sms](https://hexdocs.pm/stein_sms).

30 changes: 30 additions & 0 deletions lib/stein/sms.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule Stein.SMS do
@moduledoc """
An extension to Stein that handles SMS
"""

use Supervisor

def start_link(config) do
Supervisor.start_link(__MODULE__, config)
end

def init(config = %Stein.SMS.Development{}) do
children = [
{Stein.SMS.Config, config},
{Stein.SMS.Cache, config.cache}
]

Supervisor.init(children, strategy: :one_for_one)
end

def init(config = %Stein.SMS.Twilio{}) do
children = [
{Stein.SMS.Config, config},
{Stein.SMS.Cache, config.cache},
{Finch, name: config.finch_name}
]

Supervisor.init(children, strategy: :one_for_one)
end
end
71 changes: 71 additions & 0 deletions lib/stein/sms/cache.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
defmodule Stein.SMS.Cache do
@moduledoc """
A development cache for text messages that were sent
"""

use GenServer

defstruct ets_key: :stein_sms_cache, name: __MODULE__

require Logger

def cache(config, text_message) do
GenServer.call(config.name, {:cache, text_message})
end

def text_messages(config) do
Enum.map(keys(config.ets_key), fn key ->
{:ok, text_message} = get(config.ets_key, key)
text_message
end)
end

@doc false
def get(ets_key, key) do
case :ets.lookup(ets_key, key) do
[{^key, value}] ->
{:ok, value}

_ ->
{:error, :not_found}
end
end

@doc false
def keys(ets_key) do
key = :ets.first(ets_key)
keys(key, [], ets_key)
end

def keys(:"$end_of_table", accumulator, _ets_key), do: accumulator

def keys(current_key, accumulator, ets_key) do
next_key = :ets.next(ets_key, current_key)
keys(next_key, [current_key | accumulator], ets_key)
end

def start_link(config) do
GenServer.start_link(__MODULE__, config, name: config.name)
end

@impl true
def init(config) do
{:ok, config, {:continue, :start_cache}}
end

@impl true
def handle_continue(:start_cache, state) do
:ets.new(state.ets_key, [:set, :protected, :named_table])

{:noreply, state}
end

@impl true
def handle_call({:cache, text_message}, _from, state) do
Logger.info("Caching sent text - #{inspect(text_message)}")

:ets.insert(state.ets_key, {text_message.id, text_message})

{:reply, :ok, state}
end
end
30 changes: 30 additions & 0 deletions lib/stein/sms/config.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule Stein.SMS.Config do
@moduledoc """
Configuration cache for Stein.SMS
Start with the configuration that should be generally used in your
application. It will be stored in `:persistent_term` for quick access.
"""

use GenServer

def get() do
:persistent_term.get(__MODULE__)
end

def start_link(config) do
GenServer.start_link(__MODULE__, config)
end

@impl true
def init(config) do
{:ok, config, {:continue, :cache_config}}
end

@impl true
def handle_continue(:cache_config, state) do
:persistent_term.put(__MODULE__, state)

{:noreply, state}
end
end
40 changes: 40 additions & 0 deletions lib/stein/sms/development.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule Stein.SMS.Development do
@moduledoc """
Development service for Stein.SMS
Caches all texts being sent from your application in `Stein.SMS.Cache`
"""

alias Stein.SMS.Cache
alias Stein.SMS.TextMessage

defstruct cache: %Stein.SMS.Cache{}

@doc false
def generate_id() do
bytes =
Enum.reduce(1..4, <<>>, fn _, bytes ->
bytes <> <<Enum.random(0..255)>>
end)

Base.encode16(bytes, case: :lower)
end

defimpl Stein.SMS.Service do
alias Stein.SMS.Development

def send_text(config, from, to, message) do
text_message = %TextMessage{
id: Development.generate_id(),
from: from,
to: to,
message: message,
sent_at: DateTime.utc_now()
}

Cache.cache(config.cache, text_message)

:ok
end
end
end
11 changes: 11 additions & 0 deletions lib/stein/sms/service.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defprotocol Stein.SMS.Service do
@moduledoc """
Protocol for defining an SMS service
Currently defined services:
- `Stein.SMS.Twilio`: send texts via twilio
- `Stein.SMS.Development`: send texts in a development mode, caching texts
"""

def send_text(_config, from, to, message)
end
7 changes: 7 additions & 0 deletions lib/stein/sms/text_message.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Stein.SMS.TextMessage do
@moduledoc """
Development struct for containing information about a sent text
"""

defstruct [:id, :from, :to, :message, :sent_at]
end
83 changes: 83 additions & 0 deletions lib/stein/sms/twilio.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
defmodule Stein.SMS.Twilio do
@moduledoc """
Twilio service for Stein.SMS
Sends texts via Twilio messaging API
Configuration required:
- `account_sid`: Find this on the Twilio Dashboard
- `auth_token`: Find this on the Twilio Dashboard
"""

@enforce_keys [:account_sid, :auth_token]
defstruct [:account_sid, :auth_token, cache: %Stein.SMS.Cache{}, finch_name: Stein.SMS.Twilio]

defmodule Exception do
defexception [:body, :code, :reason, :status]

def message(struct) do
"""
Twilio failed an API request
Error Code: #{struct.code} - #{struct.reason}
Status: #{struct.status}
Body:
#{inspect(struct.body, pretty: true)}
"""
end
end

defimpl Stein.SMS.Service do
alias Stein.SMS.Twilio

@twilio_base_url "https://api.twilio.com/2010-04-01/Accounts/"

def send_text(config, from, to, message) do
basic_auth = "#{config.account_sid}:#{config.auth_token}" |> Base.encode64()
api_url = "#{@twilio_base_url}#{config.account_sid}/Messages.json"

req_headers = [
{"Authorization", "Basic #{basic_auth}"},
{"Content-Type", "application/x-www-form-urlencoded"}
]

req_body =
URI.encode_query(%{
"Body" => message,
"From" => from,
"To" => to
})

request = Finch.build(:post, api_url, req_headers, req_body)

case Finch.request(request, config.finch_name) do
{:ok, %{status: 201}} ->
:ok

{:ok, %{body: body, status: 400}} ->
body = Jason.decode!(body)

exception = %Twilio.Exception{
code: body["code"],
body: body,
reason: body["message"],
status: 400
}

{:error, exception}

{:ok, %{body: body, status: status}} ->
body = Jason.decode!(body)

exception = %Twilio.Exception{
body: body,
reason: "Unknown error",
status: status
}

{:error, exception}
end
end
end
end
29 changes: 29 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule SteinSms.MixProject do
use Mix.Project

def project do
[
app: :stein_sms,
version: "0.1.0",
elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:credo, "~> 1.1", only: [:dev, :test]},
{:finch, "~> 0.8"},
{:jason, "~> 1.0"}
]
end
end
Loading

0 comments on commit 3f102dc

Please sign in to comment.