From c9bacc8bc79fdcffa946a9477c3ed633229e7c7b Mon Sep 17 00:00:00 2001 From: TJ Date: Fri, 25 Oct 2024 18:42:29 +0900 Subject: [PATCH] create & update notes --- .gitignore | 3 +- config/runtime.exs | 2 - lib/migration/typesense/note.ex | 1 + lib/migration/typesense/photo.ex | 6 +-- lib/save_it/bot.ex | 68 ++++++++++++++------------------ lib/save_it/note_service.ex | 60 +++++++++++++++++++++------- lib/save_it/photo_service.ex | 2 +- lib/small_sdk/typesense.ex | 29 ++++++++++++-- 8 files changed, 105 insertions(+), 66 deletions(-) diff --git a/.gitignore b/.gitignore index 1df64ff..1c7fa49 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ data # scripts _local* -_stag* _dev* +_stag* +_prod* nohup.out diff --git a/config/runtime.exs b/config/runtime.exs index 047283f..faaef97 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -9,5 +9,3 @@ config :save_it, :typesense_api_key, System.get_env("TYPESENSE_API_KEY", "xyz") # optional config :save_it, :google_oauth_client_id, System.get_env("GOOGLE_OAUTH_CLIENT_ID") config :save_it, :google_oauth_client_secret, System.get_env("GOOGLE_OAUTH_CLIENT_SECRET") - -config :save_it, :web_url, System.get_env("WEB_URL", "http://localhost:4000") diff --git a/lib/migration/typesense/note.ex b/lib/migration/typesense/note.ex index 22006c7..727e73a 100644 --- a/lib/migration/typesense/note.ex +++ b/lib/migration/typesense/note.ex @@ -10,6 +10,7 @@ defmodule Migration.Typesense.Note do # note: 抉择:这个 app 核心是给予图片的视觉笔记,暂时不考虑单独 text 的笔记 # %{"name" => "photo_id", "type" => "string"}, # note: 既然不能实现 RDB reference,那么就直接存储 file_id + %{"name" => "message_id", "type" => "string"}, %{"name" => "file_id", "type" => "string"}, %{"name" => "belongs_to_id", "type" => "string"}, %{"name" => "inserted_at", "type" => "int64"}, diff --git a/lib/migration/typesense/photo.ex b/lib/migration/typesense/photo.ex index ae1abe5..fb66d48 100644 --- a/lib/migration/typesense/photo.ex +++ b/lib/migration/typesense/photo.ex @@ -17,12 +17,8 @@ defmodule Migration.Typesense.Photo do } }, %{"name" => "caption", "type" => "string", "optional" => true, "facet" => false}, - # "telegram:///" - # TODO: 不能再简单的 reset 了,reset 会导致数据丢失,应该合理 migrate 数据 - %{"name" => "url", "type" => "string"}, - # chat.id -> string + %{"name" => "file_id", "type" => "string"}, %{"name" => "belongs_to_id", "type" => "string"}, - # unix timestamp %{"name" => "inserted_at", "type" => "int64"} ], "default_sorting_field" => "inserted_at" diff --git a/lib/save_it/bot.ex b/lib/save_it/bot.ex index 60120a5..e402466 100644 --- a/lib/save_it/bot.ex +++ b/lib/save_it/bot.ex @@ -142,12 +142,11 @@ defmodule SaveIt.Bot do # end def handle( - {:command, :note, %{chat: chat, text: text, reply_to_message: reply_to_message}}, + {:command, :note, + %{message_id: message_id, chat: chat, text: text, reply_to_message: reply_to_message}}, _context ) when is_binary(text) do - Logger.debug("photo: #{inspect(reply_to_message.photo)}") - file_id = reply_to_message.photo |> List.last() |> Map.get(:file_id) case file_id do @@ -160,18 +159,17 @@ defmodule SaveIt.Bot do send_message(chat.id, "What note do you want to add?") note_content -> - # photo_url = photo_url(chat.id, file_id) - # PhotoService.update_photo!(photo_url, %{"note" => text}) - note = NoteService.create_note!(%{ content: note_content, + message_id: message_id, file_id: file_id, belongs_to_id: chat.id }) case note do nil -> send_message(chat.id, "Failed to add note.") + # TODO:nice_to_have: 添加一个 emoji 即可 _ -> send_message(chat.id, "Note added successfully.") end end @@ -187,20 +185,19 @@ defmodule SaveIt.Bot do # end # caption: nil -> find same photos - def handle({:message, %{chat: chat, caption: nil, photo: photos}}, ctx) do + def handle({:message, %{chat: chat, caption: nil, photo: photos}}, _ctx) do photo = List.last(photos) file = ExGram.get_file!(photo.file_id) photo_file_content = Telegram.download_file_content!(file.file_path) - bot_id = ctx.bot_info.id chat_id = chat.id typesense_photo = PhotoService.create_photo!(%{ image: Base.encode64(photo_file_content), caption: "", - url: photo_url(bot_id, file.file_id), + file_id: file.file_id, belongs_to_id: chat_id }) @@ -218,13 +215,12 @@ defmodule SaveIt.Bot do end # caption: contains /similar or /search -> search similar photos; otherwise, find same photos - def handle({:message, %{chat: chat, caption: caption, photo: photos}}, ctx) do + def handle({:message, %{chat: chat, caption: caption, photo: photos}}, _ctx) do photo = List.last(photos) file = ExGram.get_file!(photo.file_id) photo_file_content = Telegram.download_file_content!(file.file_path) - bot_id = ctx.bot_info.id chat_id = chat.id caption = @@ -238,7 +234,7 @@ defmodule SaveIt.Bot do PhotoService.create_photo!(%{ image: Base.encode64(photo_file_content), caption: caption, - url: photo_url(bot_id, file.file_id), + file_id: file.file_id, belongs_to_id: chat_id }) @@ -367,9 +363,24 @@ defmodule SaveIt.Bot do {:ok, nil} end - def handle({:edited_message, _msg}, _context) do - Logger.warning("this is an edited message, ignore it") - {:ok, nil} + def handle({:edited_message, msg}, _context) do + %{message_id: message_id, chat: chat, text: text} = msg + + edited_note_text = + case Regex.run(~r/\/note\s+(.*)/, text) do + [_, edited_note_text] -> edited_note_text + _ -> nil + end + + case String.contains?(text, "/note") do + true -> + note = NoteService.get_note!(message_id, chat.id) + + NoteService.update_note!(note["id"], %{"content" => edited_note_text}) + + false -> + Logger.debug("edited message: #{inspect(_msg)}") + end end def handle({:update, _update}, _context) do @@ -382,19 +393,6 @@ defmodule SaveIt.Bot do {:ok, nil} end - defp pick_file_id_from_photo_url(photo_url) do - captures = - Regex.named_captures(~r"/files/(?\d+)/(?.+)", photo_url) - - if captures == nil do - Logger.error("Invalid photo URL: #{photo_url}") - nil - else - %{"file_id" => file_id} = captures - file_id - end - end - defp answer_photos(chat_id, nil) do send_message(chat_id, "No photos found.") end @@ -408,7 +406,7 @@ defmodule SaveIt.Bot do Enum.map(similar_photos, fn photo -> %ExGram.Model.InputMediaPhoto{ type: "photo", - media: pick_file_id_from_photo_url(photo["url"]), + media: photo["file_id"], caption: "Found photos", show_caption_above_media: true } @@ -474,7 +472,7 @@ defmodule SaveIt.Bot do case file_extension(file_name) do ext when ext in [".png", ".jpg", ".jpeg"] -> {:ok, msg} = ExGram.send_photo(chat_id, content) - bot_id = msg.from.id + file_id = get_file_id(msg) image_base64 = @@ -486,7 +484,7 @@ defmodule SaveIt.Bot do PhotoService.create_photo!(%{ image: image_base64, caption: file_name, - url: photo_url(bot_id, file_id), + file_id: file_id, belongs_to_id: chat_id }) @@ -535,12 +533,4 @@ defmodule SaveIt.Bot do """) end end - - defp photo_url(bot_id, file_id) do - proxy_url = Application.fetch_env!(:save_it, :web_url) <> "/telegram/files" - - encoded_bot_id = URI.encode(bot_id |> to_string()) - encoded_file_id = URI.encode(file_id) - "#{proxy_url}/#{encoded_bot_id}/#{encoded_file_id}" - end end diff --git a/lib/save_it/note_service.ex b/lib/save_it/note_service.ex index 6a742d7..16803b7 100644 --- a/lib/save_it/note_service.ex +++ b/lib/save_it/note_service.ex @@ -3,29 +3,61 @@ defmodule SaveIt.NoteService do alias SmallSdk.Typesense - def create_note!(%{ - content: content, - file_id: file_id, - belongs_to_id: belongs_to_id - }) do + def create_note!( + %{ + message_id: message_id, + belongs_to_id: belongs_to_id + } = note_params + ) do now_unix = DateTime.utc_now() |> DateTime.to_unix() note_create_input = - %{ - content: content, - file_id: file_id - } + note_params + |> Map.put(:message_id, Integer.to_string(message_id)) |> Map.put(:belongs_to_id, Integer.to_string(belongs_to_id)) |> Map.put(:inserted_at, now_unix) |> Map.put(:updated_at, now_unix) - doc = - Typesense.create_document!( + Typesense.create_document!( + "notes", + note_create_input + ) + end + + def update_note!(id, %{} = note_params) do + now_unix = DateTime.utc_now() |> DateTime.to_unix() + + note_update_input = + note_params + |> Map.put(:updated_at, now_unix) + + Typesense.update_document!( + "notes", + id, + note_update_input + ) + end + + def get_note!(message_id, chat_id) do + docs = + Typesense.search_documents!( "notes", - note_create_input + q: "*", + query_by: "content", + filter_by: "message_id:=#{message_id} && belongs_to_id:=#{chat_id}" ) - Logger.debug("doc: #{inspect(doc)}") - doc + case docs do + nil -> + nil + + [] -> + nil + + [doc | rest] -> + Logger.warning("Found multiple notes, skipping the rest: #{inspect(rest)}") + + doc + end end end diff --git a/lib/save_it/photo_service.ex b/lib/save_it/photo_service.ex index b7aa2d8..1325949 100644 --- a/lib/save_it/photo_service.ex +++ b/lib/save_it/photo_service.ex @@ -19,7 +19,7 @@ defmodule SaveIt.PhotoService do end def update_photo(photo) do - Typesense.update_document("photos", photo.id, photo) + Typesense.update_document!("photos", photo.id, photo) end def get_photo(photo_id) do diff --git a/lib/small_sdk/typesense.ex b/lib/small_sdk/typesense.ex index 24d400d..75b27ff 100644 --- a/lib/small_sdk/typesense.ex +++ b/lib/small_sdk/typesense.ex @@ -17,7 +17,7 @@ defmodule SmallSdk.Typesense do raise "Unauthorized" %Req.Response{status: 404} -> - raise "Not Found" + nil %Req.Response{status: 409} -> raise "Conflict" @@ -40,18 +40,39 @@ defmodule SmallSdk.Typesense do handle_response(res) end + def search_documents!(collection_name, opts) do + q = Keyword.get(opts, :q, "*") + query_by = Keyword.get(opts, :query_by, "") + filter_by = Keyword.get(opts, :filter_by, "") + + req = build_request("/collections/#{collection_name}/documents/search") + + {:ok, res} = + Req.get(req, + params: %{ + q: q, + query_by: query_by, + filter_by: filter_by + } + ) + + data = handle_response(res) + + data["hits"] |> Enum.map(&Map.get(&1, "document")) + end + def get_document(collection_name, document_id) do req = build_request("/collections/#{collection_name}/documents/#{document_id}") {:ok, res} = Req.get(req) - res.body + handle_response(res) end - def update_document(collection_name, document_id, update_input) do + def update_document!(collection_name, document_id, update_input) do req = build_request("/collections/#{collection_name}/documents/#{document_id}") {:ok, res} = Req.patch(req, json: update_input) - res.body + handle_response(res) end def create_search_key() do