From f3529f81dc4529bea46769e4d032bbf42d822580 Mon Sep 17 00:00:00 2001 From: Pieter Claerhout Date: Sat, 4 Oct 2025 11:30:32 +0200 Subject: [PATCH 1/7] Initial support for put requests --- lib/nimrag/api.ex | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/nimrag/api.ex b/lib/nimrag/api.ex index 1d7c220..2b7169c 100644 --- a/lib/nimrag/api.ex +++ b/lib/nimrag/api.ex @@ -29,6 +29,18 @@ defmodule Nimrag.Api do end end + @spec put(Client.t(), Keyword.t()) :: + {:ok, Req.Response.t(), Client.t()} | {:error, Req.Response.t()} + def put(%Client{} = client, opts) do + client + |> req(opts) + |> Req.get() + |> case do + {:ok, %{status: 200} = resp} -> {:ok, resp, Req.Response.get_private(resp, :client)} + {:error, error} -> {:error, error} + end + end + @spec response_as_data({:ok, Req.Response.t(), Client.t()}, data_module :: atom()) :: {:ok, any(), Client.t()} | {:error, Req.Response.t()} @spec response_as_data({:error, any()}, data_module :: atom()) :: {:error, any()} From fd9197fbb985f2f48d3d4dd78b0e2bd3458185e2 Mon Sep 17 00:00:00 2001 From: Pieter Claerhout Date: Sat, 4 Oct 2025 11:40:00 +0200 Subject: [PATCH 2/7] Fixed the request --- lib/nimrag/api.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/nimrag/api.ex b/lib/nimrag/api.ex index 2b7169c..2d20371 100644 --- a/lib/nimrag/api.ex +++ b/lib/nimrag/api.ex @@ -34,9 +34,9 @@ defmodule Nimrag.Api do def put(%Client{} = client, opts) do client |> req(opts) - |> Req.get() + |> Req.put() |> case do - {:ok, %{status: 200} = resp} -> {:ok, resp, Req.Response.get_private(resp, :client)} + {:ok, resp} -> {:ok, resp, Req.Response.get_private(resp, :client)} {:error, error} -> {:error, error} end end From f837d2c95f73186d48af701cc2f01a03f1da0d2c Mon Sep 17 00:00:00 2001 From: Pieter Claerhout Date: Sat, 4 Oct 2025 11:44:54 +0200 Subject: [PATCH 3/7] Added post support --- lib/nimrag/api.ex | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/nimrag/api.ex b/lib/nimrag/api.ex index 2d20371..600841e 100644 --- a/lib/nimrag/api.ex +++ b/lib/nimrag/api.ex @@ -41,6 +41,18 @@ defmodule Nimrag.Api do end end + @spec post(Client.t(), Keyword.t()) :: + {:ok, Req.Response.t(), Client.t()} | {:error, Req.Response.t()} + def post(%Client{} = client, opts) do + client + |> req(opts) + |> Req.post() + |> case do + {:ok, resp} -> {:ok, resp, Req.Response.get_private(resp, :client)} + {:error, error} -> {:error, error} + end + end + @spec response_as_data({:ok, Req.Response.t(), Client.t()}, data_module :: atom()) :: {:ok, any(), Client.t()} | {:error, Req.Response.t()} @spec response_as_data({:error, any()}, data_module :: atom()) :: {:error, any()} From 5ce5dc220b8310169671c2ac643531158c9c1887 Mon Sep 17 00:00:00 2001 From: Pieter Claerhout Date: Sun, 22 Mar 2026 19:22:54 +0100 Subject: [PATCH 4/7] Fix cookie header compatibility with Req 0.5.17 Req.Request.put_header/3 now requires a binary value. Join the list returned by get_cookie/1 into a single "; "-delimited string before passing it as the cookie header. Co-Authored-By: Claude Sonnet 4.6 --- lib/nimrag/auth.ex | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/nimrag/auth.ex b/lib/nimrag/auth.ex index 1ce704b..3abf312 100644 --- a/lib/nimrag/auth.ex +++ b/lib/nimrag/auth.ex @@ -196,7 +196,10 @@ defmodule Nimrag.Auth do uri = response |> get_location() |> URI.parse() sso.client - |> Req.Request.put_header("cookie", Enum.uniq(cookie ++ get_cookie(response))) + |> Req.Request.put_header( + "cookie", + Enum.uniq(cookie ++ get_cookie(response)) |> Enum.join("; ") + ) |> Req.Request.put_header( "referer", "#{sso.url}/verifyMFA/loginEnterMfaCode" @@ -295,7 +298,7 @@ defmodule Nimrag.Auth do defp signin_req(sso, %Req.Response{} = prev_resp) do sso.client - |> Req.Request.put_header("cookie", get_cookie(prev_resp)) + |> Req.Request.put_header("cookie", get_cookie(prev_resp) |> Enum.join("; ")) |> Req.Request.put_header("referer", "#{sso.url}/embed") |> Req.get( url: "/signin", @@ -307,7 +310,7 @@ defmodule Nimrag.Auth do defp submit_signin_req(sso, %Req.Response{} = prev_resp, credentials) do with {:ok, csrf_token} <- get_csrf_token(prev_resp) do sso.client - |> Req.Request.put_header("cookie", get_cookie(prev_resp)) + |> Req.Request.put_header("cookie", get_cookie(prev_resp) |> Enum.join("; ")) |> Req.Request.put_header("referer", "#{sso.url}/signin") |> Req.post( url: "/signin", From 05c9332b234bc7fb7445c069c6798fa334a54ee3 Mon Sep 17 00:00:00 2001 From: Pieter Claerhout Date: Sun, 22 Mar 2026 19:25:28 +0100 Subject: [PATCH 5/7] Updated git ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c92e289..386782e 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ priv/plts # dot examples are not commited until they're ready! # makes it nicer to live test some APIs when local scripts examples/.* +.DS_Store From d9243f77abf938d208b38c6bedef0413f8d964aa Mon Sep 17 00:00:00 2001 From: Pieter Claerhout Date: Sun, 22 Mar 2026 19:29:44 +0100 Subject: [PATCH 6/7] Updated the Github workflow files --- .github/actions/elixir-setup/action.yml | 22 +++++++-------------- .github/workflows/elixir-build-and-test.yml | 2 +- .github/workflows/elixir-dialyzer.yml | 4 ++-- .github/workflows/elixir-quality-checks.yml | 2 +- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/.github/actions/elixir-setup/action.yml b/.github/actions/elixir-setup/action.yml index 8e8ae9e..54add5c 100644 --- a/.github/actions/elixir-setup/action.yml +++ b/.github/actions/elixir-setup/action.yml @@ -3,11 +3,9 @@ description: Checks out the code, configures Elixir, fetches dependencies, and m inputs: elixir-version: required: true - type: string description: Elixir version to set up otp-version: required: true - type: string description: OTP version to set up ################################################################# # Everything below this line is optional. @@ -18,32 +16,26 @@ inputs: ################################################################# build-deps: required: false - type: boolean - default: true + default: 'true' description: True if we should compile dependencies build-app: required: false - type: boolean - default: true + default: 'true' description: True if we should compile the application itself build-flags: required: false - type: string default: '--all-warnings' description: Flags to pass to mix compile install-rebar: required: false - type: boolean - default: true + default: 'true' description: By default, we will install Rebar (mix local.rebar --force). install-hex: required: false - type: boolean - default: true + default: 'true' description: By default, we will install Hex (mix local.hex --force). cache-key: required: false - type: string default: 'v1' description: If you need to reset the cache for some reason, you can change this key. outputs: @@ -64,7 +56,7 @@ runs: otp-version: ${{ inputs.otp-version }} - name: Get deps cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: deps/ key: deps-${{ inputs.cache-key }}-${{ runner.os }}-${{ hashFiles('**/mix.lock') }} @@ -72,7 +64,7 @@ runs: deps-${{ inputs.cache-key }}-${{ runner.os }}- - name: Get build cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: build-cache with: path: _build/${{env.MIX_ENV}}/ @@ -81,7 +73,7 @@ runs: build-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ env.MIX_ENV }}- - name: Get Hex cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: hex-cache with: path: ~/.hex diff --git a/.github/workflows/elixir-build-and-test.yml b/.github/workflows/elixir-build-and-test.yml index 1717aa3..6ee9b79 100644 --- a/.github/workflows/elixir-build-and-test.yml +++ b/.github/workflows/elixir-build-and-test.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Elixir Project uses: ./.github/actions/elixir-setup diff --git a/.github/workflows/elixir-dialyzer.yml b/.github/workflows/elixir-dialyzer.yml index d7a4674..da2c13f 100644 --- a/.github/workflows/elixir-dialyzer.yml +++ b/.github/workflows/elixir-dialyzer.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Elixir Project uses: ./.github/actions/elixir-setup @@ -34,7 +34,7 @@ jobs: # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones # Cache key based on Elixir & Erlang version (also useful when running in matrix) - name: Restore PLT cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: plt_cache with: key: plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }}-${{ hashFiles('**/*.ex') }} diff --git a/.github/workflows/elixir-quality-checks.yml b/.github/workflows/elixir-quality-checks.yml index f446052..3fd3a43 100644 --- a/.github/workflows/elixir-quality-checks.yml +++ b/.github/workflows/elixir-quality-checks.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Elixir Project uses: ./.github/actions/elixir-setup From 322434402a8257a309aae9301958ae9d00774919 Mon Sep 17 00:00:00 2001 From: Pieter Claerhout Date: Wed, 1 Apr 2026 08:03:28 +0200 Subject: [PATCH 7/7] Improve error handling in get_oauth1_token Replace pattern-match crash on HTTP response with a `with` chain that handles HTTP 429 rate limiting, unexpected status codes, request errors, and malformed response bodies as distinct tagged errors. --- lib/nimrag/auth.ex | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/nimrag/auth.ex b/lib/nimrag/auth.ex index 3abf312..de75661 100644 --- a/lib/nimrag/auth.ex +++ b/lib/nimrag/auth.ex @@ -80,7 +80,7 @@ defmodule Nimrag.Auth do now = DateTime.utc_now() - {:ok, response} = + result = client.connectapi |> Req.Request.put_header("Authorization", oauth) |> Req.get( @@ -89,18 +89,25 @@ defmodule Nimrag.Auth do user_agent: @mobile_user_agent ) - %{"oauth_token" => token, "oauth_token_secret" => secret} = - query = URI.decode_query(response.body) - - {:ok, - %OAuth1Token{ - oauth_token: token, - oauth_token_secret: secret, - domain: client.domain, - mfa_token: query["mfa_token"] || "", - # TODO: OAuth1Token, Is that 365 days true with MFA active? We'll wait and see! - expires_at: DateTime.add(now, 365, :day) - }} + with {:ok, %{status: 200} = response} <- result, + query = URI.decode_query(response.body), + {:ok, token} <- Map.fetch(query, "oauth_token"), + {:ok, secret} <- Map.fetch(query, "oauth_token_secret") do + {:ok, + %OAuth1Token{ + oauth_token: token, + oauth_token_secret: secret, + domain: client.domain, + mfa_token: query["mfa_token"] || "", + # TODO: OAuth1Token, Is that 365 days true with MFA active? We'll wait and see! + expires_at: DateTime.add(now, 365, :day) + }} + else + {:ok, %{status: 429} = response} -> {:error, {:rate_limited, response}} + {:ok, response} -> {:error, {:get_oauth1_token, response}} + {:error, _} = error -> error + :error -> {:error, :invalid_oauth1_response} + end end def maybe_refresh_oauth2_token(%Client{} = client, opts \\ []) do