Skip to content

Commit 4e86a66

Browse files
feat: update photo's caption (#24)
* first commit * refactor, show photo's caption * refactor, typesense sdk delete_document * refactor * refactor: 💡 handle_response and handle_response! * refactor: 💡 get_env * refactor: 💡 UrlHelper validate_url * chore: 🤖 typesense migration SaveIt.Migration.Typesense.Photo.migrate_photos_2024_10_29!() * refactor * fix: typesense list_documents
1 parent b74c335 commit 4e86a66

20 files changed

+471
-167
lines changed

.gitignore

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@ save_it-*.tar
2525
# Temporary files, for example, from tests.
2626
/tmp/
2727
.DS_Store
28-
data
29-
dev.sh
30-
start.sh
31-
run.sh
32-
nohup.out
3328

29+
# data
3430
_local
31+
data
32+
33+
# scripts
34+
_local*
35+
_dev*
36+
_stag*
37+
_prod*
38+
nohup.out

README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
A telegram bot can Save photos and Search photos
44

5+
## Features
6+
57
- [x] Save photos via a link
68
- [x] Search photos using semantic search
79
- [x] Find similar photos by photo
@@ -27,13 +29,8 @@ messages:
2729

2830
```
2931
/search cat
30-
3132
/search dog
32-
3333
/search girl
34-
35-
/similar photo
36-
3734
/similar photo
3835
```
3936

@@ -53,15 +50,18 @@ https://t.me/save_it_playground
5350
## Development
5451

5552
```sh
56-
# install
53+
# Install
5754
mix deps.get
5855
```
5956

6057
```sh
61-
# run
62-
export TELEGRAM_BOT_TOKEN=
63-
export TYPESENSE_URL=
64-
export TYPESENSE_API_KEY=
58+
# Start typesense
59+
docker compose up
60+
```
61+
62+
```sh
63+
# Run
64+
export TELEGRAM_BOT_TOKEN=<YOUR_TELEGRAM_BOT_TOKEN>
6565

6666
iex -S mix run --no-halt
6767
```

config/runtime.exs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,3 @@ config :save_it, :typesense_api_key, System.get_env("TYPESENSE_API_KEY", "xyz")
99
# optional
1010
config :save_it, :google_oauth_client_id, System.get_env("GOOGLE_OAUTH_CLIENT_ID")
1111
config :save_it, :google_oauth_client_secret, System.get_env("GOOGLE_OAUTH_CLIENT_SECRET")
12-
13-
config :save_it, :web_url, System.get_env("WEB_URL", "http://localhost:4000")
File renamed without changes.

docs/dev-logs/2024-10-25.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# 2024-10-25
2+
3+
## Req call typesense API alway :timeout, but typesense was updated.
4+
5+
```elixir
6+
** (MatchError) no match of right hand side value: {:error, %Req.TransportError{reason: :timeout}}
7+
```

docs/dev/readme.md

Whitespace-only changes.

docs/dev/typesense.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Typesense
2+
3+
## Typesense API Errors
4+
5+
```
6+
# 400 Bad Request - The request could not be understood due to malformed syntax.
7+
# 401 Unauthorized - Your API key is wrong.
8+
# 404 Not Found - The requested resource is not found.
9+
# 409 Conflict - When a resource already exists.
10+
# 422 Unprocessable Entity - Request is well-formed, but cannot be processed.
11+
# 503 Service Unavailable - We’re temporarily offline. Please try again later.
12+
```
13+
14+
docs: https://typesense.org/docs/27.1/api/api-errors.html#api-errors

lib/save_it/bot.ex

Lines changed: 33 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule SaveIt.Bot do
55
alias SaveIt.GoogleDrive
66
alias SaveIt.GoogleOAuth2DeviceFlow
77

8-
alias SaveIt.TypesensePhoto
8+
alias SaveIt.PhotoService
99

1010
alias SmallSdk.Telegram
1111

@@ -115,6 +115,10 @@ defmodule SaveIt.Bot do
115115
end
116116
end
117117

118+
def handle({:command, :search, %{chat: chat, text: nil}}, _context) do
119+
send_message(chat.id, "What do you want to search? animal, food, etc.")
120+
end
121+
118122
def handle({:command, :search, %{chat: chat, text: text}}, _context)
119123
when is_binary(text) do
120124
q = String.trim(text)
@@ -124,7 +128,7 @@ defmodule SaveIt.Bot do
124128
send_message(chat.id, "What do you want to search? animal, food, etc.")
125129

126130
_ ->
127-
photos = TypesensePhoto.search_photos!(q, belongs_to_id: chat.id)
131+
photos = PhotoService.search_photos!(q, belongs_to_id: chat.id)
128132

129133
answer_photos(chat.id, photos)
130134
end
@@ -134,30 +138,25 @@ defmodule SaveIt.Bot do
134138
send_message(chat.id, "Upload a photo to find similar photos.")
135139
end
136140

137-
# dev-notes: never reach here, it will be handled by handle({:message, %{chat: chat, caption: nil, photo: photos}}, ctx)
138-
# def handle({:command, :similar, %{chat: _chat, photo: photo}}, _context) do
139-
# end
140-
141141
# caption: nil -> find same photos
142-
def handle({:message, %{chat: chat, caption: nil, photo: photos}}, ctx) do
142+
def handle({:message, %{chat: chat, caption: nil, photo: photos}}, _ctx) do
143143
photo = List.last(photos)
144144

145145
file = ExGram.get_file!(photo.file_id)
146146
photo_file_content = Telegram.download_file_content!(file.file_path)
147147

148-
bot_id = ctx.bot_info.id
149148
chat_id = chat.id
150149

151150
typesense_photo =
152-
TypesensePhoto.create_photo!(%{
151+
PhotoService.create_photo!(%{
153152
image: Base.encode64(photo_file_content),
154153
caption: "",
155-
url: photo_url(bot_id, file.file_id),
154+
file_id: file.file_id,
156155
belongs_to_id: chat_id
157156
})
158157

159158
photos =
160-
TypesensePhoto.search_similar_photos!(
159+
PhotoService.search_similar_photos!(
161160
typesense_photo["id"],
162161
distance_threshold: 0.1,
163162
belongs_to_id: chat_id
@@ -170,13 +169,12 @@ defmodule SaveIt.Bot do
170169
end
171170

172171
# caption: contains /similar or /search -> search similar photos; otherwise, find same photos
173-
def handle({:message, %{chat: chat, caption: caption, photo: photos}}, ctx) do
172+
def handle({:message, %{chat: chat, caption: caption, photo: photos}}, _ctx) do
174173
photo = List.last(photos)
175174

176175
file = ExGram.get_file!(photo.file_id)
177176
photo_file_content = Telegram.download_file_content!(file.file_path)
178177

179-
bot_id = ctx.bot_info.id
180178
chat_id = chat.id
181179

182180
caption =
@@ -187,17 +185,17 @@ defmodule SaveIt.Bot do
187185
end
188186

189187
typesense_photo =
190-
TypesensePhoto.create_photo!(%{
188+
PhotoService.create_photo!(%{
191189
image: Base.encode64(photo_file_content),
192190
caption: caption,
193-
url: photo_url(bot_id, file.file_id),
191+
file_id: file.file_id,
194192
belongs_to_id: chat_id
195193
})
196194

197195
case caption do
198196
"" ->
199197
photos =
200-
TypesensePhoto.search_similar_photos!(
198+
PhotoService.search_similar_photos!(
201199
typesense_photo["id"],
202200
distance_threshold: 0.4,
203201
belongs_to_id: chat_id
@@ -207,7 +205,7 @@ defmodule SaveIt.Bot do
207205

208206
_ ->
209207
photos =
210-
TypesensePhoto.search_similar_photos!(
208+
PhotoService.search_similar_photos!(
211209
typesense_photo["id"],
212210
distance_threshold: 0.1,
213211
belongs_to_id: chat_id
@@ -310,18 +308,15 @@ defmodule SaveIt.Bot do
310308
end
311309
end
312310

313-
def handle(
314-
{:update,
315-
%ExGram.Model.Update{message: nil, edited_message: nil, channel_post: _channel_post}},
316-
_context
317-
) do
318-
Logger.warning("this is a channel post, ignore it")
311+
def handle({:edited_message, %{photo: nil}}, _context) do
312+
Logger.warning("this is an edited message, ignore it")
313+
# TODO: edit /search trigger re-search
319314
{:ok, nil}
320315
end
321316

322-
def handle({:edited_message, _msg}, _context) do
323-
Logger.warning("this is an edited message, ignore it")
324-
{:ok, nil}
317+
def handle({:edited_message, %{chat: chat, caption: caption, photo: photos}}, _context) do
318+
file_id = photos |> List.last() |> Map.get(:file_id)
319+
PhotoService.update_photo_caption!(file_id, chat.id, caption)
325320
end
326321

327322
def handle({:update, _update}, _context) do
@@ -334,19 +329,6 @@ defmodule SaveIt.Bot do
334329
{:ok, nil}
335330
end
336331

337-
defp pick_file_id_from_photo_url(photo_url) do
338-
captures =
339-
Regex.named_captures(~r"/files/(?<bot_id>\d+)/(?<file_id>.+)", photo_url)
340-
341-
if captures == nil do
342-
Logger.error("Invalid photo URL: #{photo_url}")
343-
nil
344-
else
345-
%{"file_id" => file_id} = captures
346-
file_id
347-
end
348-
end
349-
350332
defp answer_photos(chat_id, nil) do
351333
send_message(chat_id, "No photos found.")
352334
end
@@ -360,8 +342,8 @@ defmodule SaveIt.Bot do
360342
Enum.map(similar_photos, fn photo ->
361343
%ExGram.Model.InputMediaPhoto{
362344
type: "photo",
363-
media: pick_file_id_from_photo_url(photo["url"]),
364-
caption: "Found photos",
345+
media: photo["file_id"],
346+
caption: photo["caption"],
365347
show_caption_above_media: true
366348
}
367349
end)
@@ -414,19 +396,19 @@ defmodule SaveIt.Bot do
414396
Enum.each(filenames, fn filename -> bot_send_file(chat_id, filename, {:file, filename}) end)
415397
end
416398

417-
defp bot_send_file(chat_id, file_name, file_content, _opts \\ []) do
399+
defp bot_send_file(chat_id, file_name, file_content, opts \\ []) do
418400
content =
419401
case file_content do
420402
{:file, file} -> {:file, file}
421403
{:file_content, file_content, file_name} -> {:file_content, file_content, file_name}
422404
end
423405

424-
# caption = opts[:caption]
406+
caption = Keyword.get(opts, :caption, "")
425407

426408
case file_extension(file_name) do
427409
ext when ext in [".png", ".jpg", ".jpeg"] ->
428-
{:ok, msg} = ExGram.send_photo(chat_id, content)
429-
bot_id = msg.from.id
410+
{:ok, msg} = ExGram.send_photo(chat_id, content, caption: caption)
411+
430412
file_id = get_file_id(msg)
431413

432414
image_base64 =
@@ -435,21 +417,21 @@ defmodule SaveIt.Bot do
435417
{:file_content, file_content, _file_name} -> Base.encode64(file_content)
436418
end
437419

438-
TypesensePhoto.create_photo!(%{
420+
PhotoService.create_photo!(%{
439421
image: image_base64,
440-
caption: file_name,
441-
url: photo_url(bot_id, file_id),
422+
caption: caption,
423+
file_id: file_id,
442424
belongs_to_id: chat_id
443425
})
444426

445427
".mp4" ->
446-
ExGram.send_video(chat_id, content, supports_streaming: true)
428+
ExGram.send_video(chat_id, content, supports_streaming: true, caption: caption)
447429

448430
".gif" ->
449-
ExGram.send_animation(chat_id, content)
431+
ExGram.send_animation(chat_id, content, caption: caption)
450432

451433
_ ->
452-
ExGram.send_document(chat_id, content)
434+
ExGram.send_document(chat_id, content, caption: caption)
453435
end
454436
end
455437

@@ -487,12 +469,4 @@ defmodule SaveIt.Bot do
487469
""")
488470
end
489471
end
490-
491-
defp photo_url(bot_id, file_id) do
492-
proxy_url = Application.fetch_env!(:save_it, :web_url) <> "/telegram/files"
493-
494-
encoded_bot_id = URI.encode(bot_id |> to_string())
495-
encoded_file_id = URI.encode(file_id)
496-
"#{proxy_url}/#{encoded_bot_id}/#{encoded_file_id}"
497-
end
498472
end

lib/save_it/migration/typesense.ex

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
defmodule SaveIt.Migration.Typesense do
2+
alias SmallSdk.Typesense
3+
4+
import Tj.UrlHelper, only: [validate_url!: 1]
5+
6+
def create_collection!(schema) do
7+
req = build_request("/collections")
8+
res = Req.post!(req, json: schema)
9+
10+
Typesense.handle_response!(res)
11+
end
12+
13+
def update_collection!(collection_name, schema) do
14+
req = build_request("/collections/#{collection_name}")
15+
res = Req.patch!(req, json: schema)
16+
17+
Typesense.handle_response!(res)
18+
end
19+
20+
def list_collections!() do
21+
req = build_request("/collections")
22+
res = Req.get!(req)
23+
24+
Typesense.handle_response!(res)
25+
end
26+
27+
def delete_collection!(collection_name) do
28+
req = build_request("/collections/#{collection_name}")
29+
res = Req.delete!(req)
30+
31+
Typesense.handle_response!(res)
32+
end
33+
34+
defp get_env() do
35+
url = Application.fetch_env!(:save_it, :typesense_url) |> validate_url!()
36+
37+
api_key = Application.fetch_env!(:save_it, :typesense_api_key)
38+
39+
{url, api_key}
40+
end
41+
42+
defp build_request(path) do
43+
{url, api_key} = get_env()
44+
45+
Req.new(
46+
base_url: url,
47+
url: path,
48+
headers: [
49+
{"Content-Type", "application/json"},
50+
{"X-TYPESENSE-API-KEY", api_key}
51+
]
52+
)
53+
end
54+
end

0 commit comments

Comments
 (0)