summaryrefslogtreecommitdiff
path: root/lib/silmataivas
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2025-07-14 19:34:59 +0300
committerDawid Rycerz <dawid@rycerz.xyz>2025-07-14 19:34:59 +0300
commit50ce8cb96b2b218751c2fc2a6b19372f51846acc (patch)
treee2c634d2ce856062d527667d47815a05a53361c8 /lib/silmataivas
parent0ab2e5ba2b0631b28b5b1405559237b3913c878f (diff)
feat: rewrite in rust
Diffstat (limited to 'lib/silmataivas')
-rw-r--r--lib/silmataivas/application.ex37
-rw-r--r--lib/silmataivas/locations.ex104
-rw-r--r--lib/silmataivas/locations/location.ex19
-rw-r--r--lib/silmataivas/mailer.ex44
-rw-r--r--lib/silmataivas/ntfy_notifier.ex35
-rw-r--r--lib/silmataivas/release.ex136
-rw-r--r--lib/silmataivas/repo.ex20
-rw-r--r--lib/silmataivas/scheduler.ex4
-rw-r--r--lib/silmataivas/users.ex124
-rw-r--r--lib/silmataivas/users/user.ex29
-rw-r--r--lib/silmataivas/weather_poller.ex102
11 files changed, 0 insertions, 654 deletions
diff --git a/lib/silmataivas/application.ex b/lib/silmataivas/application.ex
deleted file mode 100644
index 269f48f..0000000
--- a/lib/silmataivas/application.ex
+++ /dev/null
@@ -1,37 +0,0 @@
-defmodule Silmataivas.Application do
- # See https://hexdocs.pm/elixir/Application.html
- # for more information on OTP Applications
- @moduledoc false
-
- use Application
-
- @impl true
- def start(_type, _args) do
- children = [
- SilmataivasWeb.Telemetry,
- Silmataivas.Repo,
- {DNSCluster, query: Application.get_env(:silmataivas, :dns_cluster_query, :ignore)},
- {Phoenix.PubSub, name: Silmataivas.PubSub},
- # Start the Finch HTTP client for sending emails
- {Finch, name: Silmataivas.Finch},
- # Start a worker by calling: Silmataivas.Worker.start_link(arg)
- # {Silmataivas.Worker, arg},
- # Start to serve requests, typically the last entry
- SilmataivasWeb.Endpoint,
- Silmataivas.Scheduler
- ]
-
- # See https://hexdocs.pm/elixir/Supervisor.html
- # for other strategies and supported options
- opts = [strategy: :one_for_one, name: Silmataivas.Supervisor]
- Supervisor.start_link(children, opts)
- end
-
- # Tell Phoenix to update the endpoint configuration
- # whenever the application is updated.
- @impl true
- def config_change(changed, _new, removed) do
- SilmataivasWeb.Endpoint.config_change(changed, removed)
- :ok
- end
-end
diff --git a/lib/silmataivas/locations.ex b/lib/silmataivas/locations.ex
deleted file mode 100644
index 2fc33dc..0000000
--- a/lib/silmataivas/locations.ex
+++ /dev/null
@@ -1,104 +0,0 @@
-defmodule Silmataivas.Locations do
- @moduledoc """
- The Locations context.
- """
-
- import Ecto.Query, warn: false
- alias Silmataivas.Repo
-
- alias Silmataivas.Locations.Location
-
- @doc """
- Returns the list of locations.
-
- ## Examples
-
- iex> list_locations()
- [%Location{}, ...]
-
- """
- def list_locations do
- Repo.all(Location)
- end
-
- @doc """
- Gets a single location.
-
- Raises `Ecto.NoResultsError` if the Location does not exist.
-
- ## Examples
-
- iex> get_location!(123)
- %Location{}
-
- iex> get_location!(456)
- ** (Ecto.NoResultsError)
-
- """
- def get_location!(id), do: Repo.get!(Location, id)
-
- @doc """
- Creates a location.
-
- ## Examples
-
- iex> create_location(%{field: value})
- {:ok, %Location{}}
-
- iex> create_location(%{field: bad_value})
- {:error, %Ecto.Changeset{}}
-
- """
- def create_location(attrs \\ %{}) do
- %Location{}
- |> Location.changeset(attrs)
- |> Repo.insert()
- end
-
- @doc """
- Updates a location.
-
- ## Examples
-
- iex> update_location(location, %{field: new_value})
- {:ok, %Location{}}
-
- iex> update_location(location, %{field: bad_value})
- {:error, %Ecto.Changeset{}}
-
- """
- def update_location(%Location{} = location, attrs) do
- location
- |> Location.changeset(attrs)
- |> Repo.update()
- end
-
- @doc """
- Deletes a location.
-
- ## Examples
-
- iex> delete_location(location)
- {:ok, %Location{}}
-
- iex> delete_location(location)
- {:error, %Ecto.Changeset{}}
-
- """
- def delete_location(%Location{} = location) do
- Repo.delete(location)
- end
-
- @doc """
- Returns an `%Ecto.Changeset{}` for tracking location changes.
-
- ## Examples
-
- iex> change_location(location)
- %Ecto.Changeset{data: %Location{}}
-
- """
- def change_location(%Location{} = location, attrs \\ %{}) do
- Location.changeset(location, attrs)
- end
-end
diff --git a/lib/silmataivas/locations/location.ex b/lib/silmataivas/locations/location.ex
deleted file mode 100644
index 7da7290..0000000
--- a/lib/silmataivas/locations/location.ex
+++ /dev/null
@@ -1,19 +0,0 @@
-defmodule Silmataivas.Locations.Location do
- use Ecto.Schema
- import Ecto.Changeset
-
- schema "locations" do
- field :latitude, :float
- field :longitude, :float
- field :user_id, :id
-
- timestamps(type: :utc_datetime)
- end
-
- @doc false
- def changeset(location, attrs) do
- location
- |> cast(attrs, [:latitude, :longitude, :user_id])
- |> validate_required([:latitude, :longitude, :user_id])
- end
-end
diff --git a/lib/silmataivas/mailer.ex b/lib/silmataivas/mailer.ex
deleted file mode 100644
index 3c11436..0000000
--- a/lib/silmataivas/mailer.ex
+++ /dev/null
@@ -1,44 +0,0 @@
-defmodule Silmataivas.Mailer do
- use Swoosh.Mailer, otp_app: :silmataivas
- require Logger
-
- def send_alert(
- email,
- %{
- "main" => %{"temp" => temp},
- "wind" => %{"speed" => speed},
- "dt_txt" => time_str
- } = entry
- ) do
- rain_mm = get_in(entry, ["rain", "3h"]) || 0.0
- wind_kmh = speed * 3.6
-
- import Swoosh.Email
-
- body = """
- 🚨 Weather alert for your location (#{time_str}):
-
- 🌬️ Wind: #{Float.round(wind_kmh, 1)} km/h
- 🌧️ Rain: #{rain_mm} mm
- 🌡️ Temperature: #{temp} °C
-
- Stay safe,
- — Silmätaivas
- """
-
- email_struct =
- new()
- |> to(email)
- |> from({"Silmätaivas Alerts", "silmataivas@rycerz.cloud"})
- |> subject("⚠️ Weather Alert for Your Location")
- |> text_body(body)
-
- case deliver(email_struct) do
- {:ok, response} ->
- Logger.info("📨 Email sent via SES: #{inspect(response)}")
-
- {:error, reason} ->
- Logger.error("❌ Failed to send email: #{inspect(reason)}")
- end
- end
-end
diff --git a/lib/silmataivas/ntfy_notifier.ex b/lib/silmataivas/ntfy_notifier.ex
deleted file mode 100644
index 26815db..0000000
--- a/lib/silmataivas/ntfy_notifier.ex
+++ /dev/null
@@ -1,35 +0,0 @@
-defmodule Silmataivas.Notifications.NtfyNotifier do
- @moduledoc """
- Sends push notifications using ntfy.sh.
- """
-
- @ntfy_url System.get_env("NTFY_URL") || "https://ntfy.sh"
-
- def send_alert(
- topic,
- %{
- "main" => %{"temp" => temp},
- "wind" => %{"speed" => speed},
- "dt_txt" => time_str
- } = entry
- ) do
- rain_mm = get_in(entry, ["rain", "3h"]) || 0.0
- wind_kmh = speed * 3.6
-
- message = """
- 🚨 Weather alert for your location (#{time_str}):
-
- 🌬️ Wind: #{Float.round(wind_kmh, 1)} km/h
- 🌧️ Rain: #{rain_mm} mm
- 🌡️ Temperature: #{temp} °C
-
- Stay safe,
- — Silmätaivas
- """
-
- Req.post("#{@ntfy_url}/#{topic}",
- headers: [{"Priority", "5"}],
- body: message
- )
- end
-end
diff --git a/lib/silmataivas/release.ex b/lib/silmataivas/release.ex
deleted file mode 100644
index 4fc9e93..0000000
--- a/lib/silmataivas/release.ex
+++ /dev/null
@@ -1,136 +0,0 @@
-defmodule Silmataivas.Release do
- @moduledoc """
- Release tasks for Silmataivas application.
-
- This module provides functions to run Ecto migrations in a
- compiled release, supporting both SQLite and PostgreSQL backends.
- """
-
- @app :silmataivas
-
- @doc """
- Creates a new user with optional user ID and role.
-
- ## Parameters
- * `user_id` - An optional user ID to use. If not provided, a UUID will be generated.
- * `role` - An optional role, must be either "user" or "admin". Defaults to "user".
-
- ## Examples
- Silmataivas.Release.new_user()
- Silmataivas.Release.new_user("custom_user_id")
- Silmataivas.Release.new_user("custom_user_id", "admin")
- """
- def new_user(user_id \\ nil, role \\ "user") do
- # Create the new user
- load_app()
- start_repos()
-
- # Validate role
- unless role in ["user", "admin"] do
- IO.puts("\n❌ Invalid role: #{role}. Role must be either \"user\" or \"admin\".")
- exit({:shutdown, 1})
- end
-
- user_id = user_id || Ecto.UUID.generate()
- user_params = %{user_id: user_id, role: role}
-
- case Silmataivas.Users.create_user(user_params) do
- {:ok, user} ->
- IO.puts("\n✅ User created successfully!")
- IO.puts(" User ID (API token): #{user.user_id}")
- IO.puts(" Role: #{user.role}")
-
- {:error, changeset} ->
- IO.puts("\n❌ Failed to create user:")
- IO.inspect(changeset.errors)
- end
- end
-
- def migrate do
- load_app()
-
- for repo <- repos() do
- {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
- end
- end
-
- def rollback(repo, version) do
- load_app()
-
- {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
- end
-
- def create_db do
- load_app()
-
- for repo <- repos() do
- # Create the database if it doesn't exist
- adapter = get_repo_adapter(repo)
-
- case adapter.storage_up(repo.config()) do
- :ok ->
- IO.puts("Database for #{inspect(repo)} created successfully")
-
- {:error, :already_up} ->
- IO.puts("Database for #{inspect(repo)} already exists")
-
- {:error, reason} ->
- IO.warn("Database for #{inspect(repo)} failed to create: #{inspect(reason)}")
- end
- end
- end
-
- def setup do
- # Create the database and then run migrations
- create_db()
- migrate()
- end
-
- def db_info do
- load_app()
-
- for repo <- repos() do
- adapter = get_repo_adapter(repo)
- config = repo.config()
-
- IO.puts("Repository: #{inspect(repo)}")
- IO.puts("Adapter: #{inspect(adapter)}")
-
- case adapter do
- Ecto.Adapters.SQLite3 ->
- db_path = config[:database] || "default.db"
- IO.puts("Database path: #{db_path}")
-
- Ecto.Adapters.Postgres ->
- hostname = config[:hostname] || "localhost"
- database = config[:database] || "default"
- IO.puts("Host: #{hostname}, Database: #{database}")
-
- _ ->
- IO.puts("Config: #{inspect(config)}")
- end
-
- IO.puts("---")
- end
- end
-
- defp get_repo_adapter(repo) do
- repo.config()[:adapter]
- end
-
- defp start_repos do
- {:ok, _} = Application.ensure_all_started(:ecto_sql)
-
- for repo <- repos() do
- {:ok, _} = repo.start_link(pool_size: 2)
- end
- end
-
- defp repos do
- Application.fetch_env!(@app, :ecto_repos)
- end
-
- defp load_app do
- Application.load(@app)
- end
-end
diff --git a/lib/silmataivas/repo.ex b/lib/silmataivas/repo.ex
deleted file mode 100644
index d1bbcca..0000000
--- a/lib/silmataivas/repo.ex
+++ /dev/null
@@ -1,20 +0,0 @@
-defmodule Silmataivas.Repo do
- use Ecto.Repo,
- otp_app: :silmataivas,
- adapter: Ecto.Adapters.SQLite3
-
- @doc """
- Dynamic adapter configuration based on application environment.
-
- This will be automatically called by Ecto during startup.
- """
- def init(_type, config) do
- # Check for adapter in config, fall back to Ecto.Adapters.SQLite3
- adapter =
- config[:adapter] ||
- Application.get_env(:silmataivas, Silmataivas.Repo, [])[:adapter] ||
- Ecto.Adapters.SQLite3
-
- {:ok, Keyword.put(config, :adapter, adapter)}
- end
-end
diff --git a/lib/silmataivas/scheduler.ex b/lib/silmataivas/scheduler.ex
deleted file mode 100644
index 3e04f7e..0000000
--- a/lib/silmataivas/scheduler.ex
+++ /dev/null
@@ -1,4 +0,0 @@
-# lib/silmataivas/scheduler.ex
-defmodule Silmataivas.Scheduler do
- use Quantum, otp_app: :silmataivas
-end
diff --git a/lib/silmataivas/users.ex b/lib/silmataivas/users.ex
deleted file mode 100644
index 1fcefd4..0000000
--- a/lib/silmataivas/users.ex
+++ /dev/null
@@ -1,124 +0,0 @@
-defmodule Silmataivas.Users do
- @moduledoc """
- The Users context.
- """
-
- import Ecto.Query, warn: false
- alias Silmataivas.Repo
-
- alias Silmataivas.Users.User
-
- @doc """
- Returns the list of users.
-
- ## Examples
-
- iex> list_users()
- [%User{}, ...]
-
- """
- def list_users do
- Repo.all(User)
- end
-
- @doc """
- Gets a single user.
-
- Raises `Ecto.NoResultsError` if the User does not exist.
-
- ## Examples
-
- iex> get_user!(123)
- %User{}
-
- iex> get_user!(456)
- ** (Ecto.NoResultsError)
-
- """
- def get_user!(id), do: Repo.get!(User, id)
-
- @doc """
- Gets a user by user_id.
-
- ## Examples
-
- iex> get_user_by_user_id("some_user_id")
- %User{}
-
- iex> get_user_by_user_id("non_existent_user_id")
- nil
-
- """
- def get_user_by_user_id(user_id) do
- Repo.get_by(User, user_id: user_id)
- end
-
- @doc """
- Creates a user.
-
- ## Examples
-
- iex> create_user(%{field: value})
- {:ok, %User{}}
-
- iex> create_user(%{field: bad_value})
- {:error, %Ecto.Changeset{}}
-
- """
- def create_user(attrs \\ %{}) do
- %User{}
- |> User.changeset(attrs)
- |> Repo.insert()
- end
-
- @doc """
- Updates a user.
-
- ## Examples
-
- iex> update_user(user, %{field: new_value})
- {:ok, %User{}}
-
- iex> update_user(user, %{field: bad_value})
- {:error, %Ecto.Changeset{}}
-
- """
- def update_user(%User{} = user, attrs) do
- user
- |> User.changeset(attrs)
- |> Repo.update()
- end
-
- @doc """
- Deletes a user.
-
- ## Examples
-
- iex> delete_user(user)
- {:ok, %User{}}
-
- iex> delete_user(user)
- {:error, %Ecto.Changeset{}}
-
- """
- def delete_user(%User{} = user) do
- Repo.delete(user)
- end
-
- @doc """
- Returns an `%Ecto.Changeset{}` for tracking user changes.
-
- ## Examples
-
- iex> change_user(user)
- %Ecto.Changeset{data: %User{}}
-
- """
- def change_user(%User{} = user, attrs \\ %{}) do
- User.changeset(user, attrs)
- end
-
- def list_users_with_locations do
- Repo.all(from u in User, preload: [:location])
- end
-end
diff --git a/lib/silmataivas/users/user.ex b/lib/silmataivas/users/user.ex
deleted file mode 100644
index b0746cd..0000000
--- a/lib/silmataivas/users/user.ex
+++ /dev/null
@@ -1,29 +0,0 @@
-defmodule Silmataivas.Users.User do
- use Ecto.Schema
- import Ecto.Changeset
- alias Silmataivas.Repo
-
- @roles ["user", "admin"]
-
- schema "users" do
- field :user_id, :string
- field :role, :string, default: "user"
- has_one :location, Silmataivas.Locations.Location
- timestamps(type: :utc_datetime)
- end
-
- @doc false
- def changeset(user, attrs) do
- user
- |> cast(attrs, [:user_id, :role])
- |> validate_required([:user_id])
- |> validate_inclusion(:role, @roles)
- |> unique_constraint(:user_id)
- end
-
- def create_user(attrs \\ %{}) do
- %__MODULE__{}
- |> changeset(attrs)
- |> Repo.insert()
- end
-end
diff --git a/lib/silmataivas/weather_poller.ex b/lib/silmataivas/weather_poller.ex
deleted file mode 100644
index b42b184..0000000
--- a/lib/silmataivas/weather_poller.ex
+++ /dev/null
@@ -1,102 +0,0 @@
-defmodule Silmataivas.WeatherPoller do
- require Logger
- alias Silmataivas.{Users, Notifications.NtfyNotifier}
-
- @api_url "https://api.openweathermap.org/data/2.5/forecast"
- # Check forecasts within the next 24 hours
- @alert_window_hours 24
-
- def check_all do
- Logger.info("🔄 Checking weather forecast for all users...")
-
- Users.list_users_with_locations()
- |> Enum.each(&check_user_weather/1)
- end
-
- def check_user_weather(%{user_id: user_id, location: %{latitude: lat, longitude: lon}} = _user) do
- case fetch_forecast(lat, lon) do
- {:ok, forecasts} ->
- case find_first_alert_entry(forecasts) do
- nil -> :ok
- entry -> NtfyNotifier.send_alert(user_id, entry)
- end
-
- {:error, reason} ->
- Logger.error("❌ Error fetching forecast for user #{user_id}: #{inspect(reason)}")
- end
- end
-
- # Add this clause to handle users with missing location data
- def check_user_weather(%{user_id: user_id} = user) do
- Logger.warning(
- "⚠️ User #{user_id} has missing or incomplete location data: #{inspect(user)}",
- []
- )
-
- :ok
- end
-
- # Add a catch-all clause to handle unexpected data formats
- def check_user_weather(invalid_user) do
- Logger.error("❌ Invalid user data structure: #{inspect(invalid_user)}")
- :ok
- end
-
- defp fetch_forecast(lat, lon) do
- api_key = Application.fetch_env!(:silmataivas, :openweathermap_api_key)
-
- Req.get(
- url: @api_url,
- params: [
- lat: lat,
- lon: lon,
- units: "metric",
- appid: api_key
- ]
- )
- |> case do
- {:ok, %{status: 200, body: %{"list" => forecast_list}}} ->
- {:ok, forecast_list}
-
- {:ok, %{status: code, body: body}} ->
- {:error, {code, body}}
-
- error ->
- error
- end
- end
-
- defp dangerous_conditions?(
- %{
- "main" => %{"temp" => temp},
- "wind" => %{"speed" => speed},
- "dt_txt" => time_str
- } = entry
- ) do
- rain_mm = get_in(entry, ["rain", "3h"]) || 0.0
- wind_kmh = speed * 3.6
-
- cond do
- wind_kmh > 80 -> log_reason("Wind", wind_kmh, time_str)
- rain_mm > 40 -> log_reason("Rain", rain_mm, time_str)
- temp < 0 -> log_reason("Temperature", temp, time_str)
- true -> false
- end
- end
-
- defp find_first_alert_entry(forecast_list) do
- now = DateTime.utc_now()
-
- forecast_list
- |> Enum.take_while(fn %{"dt" => ts} ->
- forecast_time = DateTime.from_unix!(ts)
- DateTime.diff(forecast_time, now, :hour) <= @alert_window_hours
- end)
- |> Enum.find(&dangerous_conditions?/1)
- end
-
- defp log_reason(type, value, time_str) do
- Logger.info("🚨 #{type} threshold exceeded: #{value} at #{time_str}")
- true
- end
-end