Skip to content

Commit 2d8c3e9

Browse files
committed
Merge branch 'main' into fly
2 parents 03103b9 + 7d35ce6 commit 2d8c3e9

12 files changed

Lines changed: 230 additions & 145 deletions

File tree

lib/wik/telegram.ex

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
defmodule Wik.Telegram do
2+
@login_ttl :timer.hours(24)
3+
4+
def bot_token, do: Application.get_env(:wik, :bot_token)
5+
6+
def valid_telegram_auth?(params, miniapp?) do
7+
# Get the received hash from params and remove it from the map.
8+
check_hash = Map.get(params, "hash") |> String.downcase()
9+
10+
# Compute the secret key: SHA256(bot_token) in binary.
11+
secret_key =
12+
if miniapp? do
13+
:crypto.mac(:hmac, :sha256, "WebAppData", bot_token())
14+
else
15+
:crypto.hash(:sha256, bot_token())
16+
end
17+
18+
# Create the data-check string.
19+
data_check_string =
20+
params
21+
|> Map.delete("hash")
22+
|> Enum.map(fn {k, v} -> "#{k}=#{v}" end)
23+
|> Enum.sort()
24+
|> Enum.join("\n")
25+
26+
# Compute the HMAC-SHA256 of the data-check string with the secret key.
27+
computed_hash =
28+
:crypto.mac(:hmac, :sha256, secret_key, data_check_string)
29+
|> Base.encode16(case: :lower)
30+
|> String.downcase()
31+
32+
# Optionally, check that the authentication is not older than 24 hours.
33+
auth_date =
34+
case Map.get(params, "auth_date") do
35+
nil -> 0
36+
date when is_binary(date) -> String.to_integer(date)
37+
date when is_integer(date) -> date
38+
end
39+
40+
current_time = System.os_time(:second)
41+
42+
outdated? = current_time - auth_date > @login_ttl
43+
44+
cond do
45+
computed_hash != check_hash ->
46+
false
47+
48+
outdated? ->
49+
false
50+
51+
true ->
52+
true
53+
end
54+
end
55+
56+
def fetch_user_groups(telegram_user_id) do
57+
# For each group, check if the user is a member.
58+
all_groups = Wik.Groups.list_groups()
59+
60+
filtered =
61+
all_groups
62+
|> Enum.filter(fn group ->
63+
user_member_of?(group, telegram_user_id, bot_token())
64+
end)
65+
66+
serialized =
67+
filtered
68+
|> Enum.map(fn group ->
69+
%{
70+
id: group.id,
71+
name: group.name,
72+
slug: group.slug
73+
}
74+
end)
75+
76+
serialized
77+
end
78+
79+
defp user_member_of?(group, telegram_user_id, bot_token) do
80+
url =
81+
"https://api.telegram.org/bot#{bot_token}/getChatMember?chat_id=#{group.id}&user_id=#{telegram_user_id}"
82+
83+
req = Finch.build(:get, url)
84+
85+
case Finch.request(req, WikWeb.Finch) do
86+
{:ok, %{status: 200, body: body}} ->
87+
case Jason.decode(body) do
88+
{:ok, %{"ok" => true, "result" => %{"status" => status}}} ->
89+
status in ["member", "administrator", "creator"]
90+
91+
_ ->
92+
false
93+
end
94+
95+
_ ->
96+
false
97+
end
98+
end
99+
end

lib/wik/user.ex

Lines changed: 0 additions & 56 deletions
This file was deleted.

lib/wik/users.ex

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,22 @@ defmodule Wik.Users do
88

99
alias Wik.Users.User
1010

11-
def persist_session_user(session_user) do
12-
session_user
13-
|> Map.put(:telegram_id, session_user.id)
14-
|> Map.delete(:id)
15-
|> create_or_update_user_by_telegram_id()
11+
@doc """
12+
Persists a session user to the database and returns the database user.
13+
"""
14+
15+
# def persist_session_user(session_user) do
16+
# session_user
17+
# |> create_or_update_user_by_telegram_id()
18+
# end
19+
20+
def find_user_by_telegram_id(telegram_id) do
21+
Repo.one(from u in User, where: u.telegram_id == ^telegram_id)
1622
end
1723

18-
defp create_or_update_user_by_telegram_id(user_data) do
19-
case Repo.one(from u in User, where: u.telegram_id == ^user_data.telegram_id) do
24+
def create_or_update_user_by_telegram_id(user_data) do
25+
# case Repo.one(from u in User, where: u.telegram_id == ^user_data.telegram_id) do
26+
case find_user_by_telegram_id(user_data.telegram_id) do
2027
nil ->
2128
create_user(user_data)
2229

@@ -118,4 +125,32 @@ defmodule Wik.Users do
118125
def change_user(%User{} = user, attrs \\ %{}) do
119126
User.changeset(user, attrs)
120127
end
128+
129+
@doc """
130+
Updates a user's last_seen timestamp.
131+
Only updates if the last update was more than "interval" seconds ago to reduce DB load.
132+
"""
133+
def update_last_seen(%User{} = user) do
134+
now = DateTime.utc_now()
135+
interval = 60
136+
time_elapsed = DateTime.diff(now, user.last_seen, :second)
137+
update? = user.last_seen == nil || time_elapsed > interval
138+
139+
if update?, do: update_user(user, %{last_seen: now})
140+
end
141+
142+
def update_last_seen(user_id) do
143+
# Convert to string if it's not already
144+
# user_id = to_string(user_id)
145+
146+
case Repo.get(User, user_id) do
147+
nil ->
148+
# User doesn't exist in the database
149+
IO.warn("[Users.update_last_seen(user_id)] User with ID #{user_id} not found.")
150+
{:error, :user_not_found}
151+
152+
user ->
153+
update_last_seen(user)
154+
end
155+
end
121156
end

lib/wik/users/user.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ defmodule Wik.Users.User do
2626
:photo_url,
2727
:last_seen
2828
])
29+
|> update_change(:telegram_id, &to_string/1)
2930
|> unique_constraint(:telegram_id)
3031
|> validate_required([
3132
:telegram_id,

lib/wik_web/controllers/session_controller.ex

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ defmodule WikWeb.SessionController do
77
|> redirect(to: "/")
88
end
99

10-
def dev_login(conn, params) do
11-
username = params["user"]
12-
10+
def dev_login(conn, _params) do
1311
all_groups = Wik.Groups.list_groups()
1412

1513
serialized =
@@ -25,20 +23,22 @@ defmodule WikWeb.SessionController do
2523
img =
2624
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcBAMAAACAI8KnAAABg2lDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpSKVDlYQcQhSneyioo61CkWoEGqFVh1Mrp/QpCFJcXEUXAsOfixWHVycdXVwFQTBDxB3wUnRRUr8X1JoEePBcT/e3XvcvQOERoWpZlcMUDXLSCXiYia7KgZeISCEAYxgRmamPidJSXiOr3v4+HoX5Vne5/4cfbm8yQCfSBxjumERbxBPb1o6533iMCvJOeJz4nGDLkj8yHXF5TfORYcFnhk20ql54jCxWOxgpYNZyVCJp4gjOVWjfCHjco7zFme1UmOte/IXBvPayjLXaQ4jgUUsQYIIBTWUUYGFKK0aKSZStB/38A85folcCrnKYORYQBUqZMcP/ge/uzULkxNuUjAOdL/Y9scoENgFmnXb/j627eYJ4H8GrrS2v9oAZj9Jr7e1yBEQ2gYurtuasgdc7gCDT7psyI7kpykUCsD7GX1TFui/BXrX3N5a+zh9ANLUVfIGODgExoqUve7x7p7O3v490+rvB903ctGnToVWAAAAIVBMVEVKHBxKHD5vKl68TqDKcrS8oE5OvLyFvE6eynK62Zv36fNZSEXTAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+kCDxUqHL67UiAAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAZklEQVQY02OYiQDT0mYy4OROSw1NQ+e6IIBbmgsBrrGxCULAGJ3LYGzitWoVkOm1aolLBzbuQkEg11GKMLcDL7cc2d5yIrjOxiBg4l6OyS0vBjKVjJWUjc2xcSECxiAeMVwEQOMCAM+GiLYSTcf8AAAAAElFTkSuQmCC"
2725

28-
user = %{
29-
id: username,
26+
dev_user_data = %{
27+
telegram_id: "devuser1",
28+
username: "devuser1",
3029
first_name: "Dev",
3130
last_name: "User",
32-
auth_date: "2023-01-01",
33-
username: username,
3431
photo_url: img,
32+
auth_date: "2023-01-01",
3533
member_of: serialized
3634
}
3735

38-
Wik.Users.persist_session_user(user)
36+
{:ok, dbuser} = Wik.Users.create_or_update_user_by_telegram_id(dev_user_data)
37+
38+
session_user = Map.put(dev_user_data, :id, dbuser.id)
3939

4040
conn
41-
|> put_session(:user, user)
41+
|> put_session(:user, session_user)
4242
|> configure_session(renew: true)
4343
|> redirect(to: get_session(conn, :redirect_after_login) || "/")
4444
end
Lines changed: 30 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,19 @@
11
defmodule WikWeb.TelegramAuthController do
22
use WikWeb, :controller
3-
alias Wik.User
4-
@login_ttl :timer.hours(24)
3+
alias Wik.Telegram
54
require Logger
65

7-
defp bot_token, do: Application.get_env(:wik, :bot_token)
8-
9-
def callback(conn, params) do
10-
# params is a map with Telegram authentication data:
11-
# e.g. %{"id" => "...", "first_name" => "...", "auth_date" => "...", "hash" => "..."}
12-
13-
if valid_telegram_auth?(params, false) do
14-
user =
15-
User.get_or_create_from_telegram(params, bot_token())
16-
|> Map.delete("hash")
17-
|> Map.delete("auth_date")
18-
19-
Wik.Users.persist_session_user(user)
6+
def callback(conn, telegram_callback_response) do
7+
if Telegram.valid_telegram_auth?(telegram_callback_response, false) do
8+
session_user = make_session_user(telegram_callback_response)
209

2110
conn
22-
|> put_session(:user, user)
11+
|> put_session(:user, session_user)
2312
|> configure_session(renew: true)
2413
|> redirect(to: get_session(conn, :redirect_after_login) || "/")
2514
else
15+
Logger.error("Invalid Telegram login")
16+
2617
conn
2718
|> put_flash(:error, "Invalid Telegram login")
2819
|> redirect(to: "/")
@@ -32,15 +23,17 @@ defmodule WikWeb.TelegramAuthController do
3223
def miniapp(conn, _params) do
3324
with ["tma " <> init_data_raw] <- get_req_header(conn, "authorization"),
3425
params <- URI.decode_query(init_data_raw),
35-
true <- valid_telegram_auth?(params, true) do
36-
{:ok, params} = JSON.decode(params["user"])
26+
true <- Telegram.valid_telegram_auth?(params, true) do
27+
{:ok, telegram_callback_response} = JSON.decode(params["user"])
3728

38-
user = User.get_or_create_from_telegram(params, bot_token())
39-
Wik.Users.persist_session_user(user)
29+
session_user = make_session_user(telegram_callback_response)
4030

4131
conn
42-
|> put_session(:user, user)
43-
|> put_flash(:info, "Welcome #{user.first_name} #{user.last_name} (#{user.username})!")
32+
|> put_session(:user, session_user)
33+
|> put_flash(
34+
:info,
35+
"Welcome #{session_user.first_name} #{session_user.last_name} (#{session_user.username})!"
36+
)
4437
|> json(%{success: true})
4538
else
4639
_ ->
@@ -52,53 +45,23 @@ defmodule WikWeb.TelegramAuthController do
5245
end
5346
end
5447

55-
defp valid_telegram_auth?(params, miniapp?) do
56-
# Get the received hash from params and remove it from the map.
57-
check_hash = Map.get(params, "hash") |> String.downcase()
58-
59-
# Compute the secret key: SHA256(bot_token) in binary.
60-
secret_key =
61-
if miniapp? do
62-
:crypto.mac(:hmac, :sha256, "WebAppData", bot_token())
63-
else
64-
:crypto.hash(:sha256, bot_token())
65-
end
66-
67-
# Create the data-check string.
68-
data_check_string =
69-
params
70-
|> Map.delete("hash")
71-
|> Enum.map(fn {k, v} -> "#{k}=#{v}" end)
72-
|> Enum.sort()
73-
|> Enum.join("\n")
48+
def make_session_user(res) do
49+
telegram_user_data = %{
50+
telegram_id: "#{res["id"]}",
51+
first_name: res["first_name"],
52+
last_name: res["last_name"],
53+
auth_date: res["auth_date"],
54+
username: res["username"],
55+
photo_url: res["photo_url"]
56+
}
7457

75-
# Compute the HMAC-SHA256 of the data-check string with the secret key.
76-
computed_hash =
77-
:crypto.mac(:hmac, :sha256, secret_key, data_check_string)
78-
|> Base.encode16(case: :lower)
79-
|> String.downcase()
58+
{:ok, dbuser} = Wik.Users.create_or_update_user_by_telegram_id(telegram_user_data)
8059

81-
# Optionally, check that the authentication is not older than 24 hours.
82-
auth_date =
83-
case Map.get(params, "auth_date") do
84-
nil -> 0
85-
date when is_binary(date) -> String.to_integer(date)
86-
date when is_integer(date) -> date
87-
end
60+
session_user =
61+
telegram_user_data
62+
|> Map.put(:id, dbuser.id)
63+
|> Map.put(:member_of, Telegram.fetch_user_groups(res["id"]))
8864

89-
current_time = System.os_time(:second)
90-
91-
outdated? = current_time - auth_date > @login_ttl
92-
93-
cond do
94-
computed_hash != check_hash ->
95-
false
96-
97-
outdated? ->
98-
false
99-
100-
true ->
101-
true
102-
end
65+
session_user
10366
end
10467
end

0 commit comments

Comments
 (0)