Skip to content

Commit

Permalink
石头汤
Browse files Browse the repository at this point in the history
  • Loading branch information
ThaddeusJiang committed Jul 19, 2024
0 parents commit 7969aad
Show file tree
Hide file tree
Showing 21 changed files with 401 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
20 changes: 20 additions & 0 deletions .github/workflows/fly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Fly Deploy
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- run:
flyctl secrets set TELEGRAM_BOT_TOKEN=${{ secrets.TELEGRAM_BOT_TOKEN }} AIER_API_TOKEN=${{ secrets.AIER_API_TOKEN }} --stage
# --stage Set secrets but skip deployment for machine apps
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
29 changes: 29 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
aier_bot-*.tar

# Temporary files, for example, from tests.
/tmp/
.DS_Store
downloads
dev.sh
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# AierBot

A telegram bot to sync messages to https://aier.app

![save memo to AIer.app](./docs/assets/save-memo-to-aier.gif)

## Build with

- [Elixir](https://elixir-lang.org/)
- [ex_gram](https://github.com/rockneurotiko/ex_gram)

## Usage

TODO:

## Development

```sh
mix deps.get

export TELEGRAM_BOT_TOKEN=
export AIER_API_TOKEN=
export OPENAI_API_KEY=

mix run --no-halt
```

## Deployment

### CI/CD

GitHub Actions

Secrets:

- `TELEGRAM_BOT_TOKEN`
- `AIER_API_TOKEN`
- `FLY_API_TOKEN`
3 changes: 3 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Config

config :tesla, :adapter, Tesla.Adapter.Hackney
5 changes: 5 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Config

config :aier_bot, :telegram_bot_token, System.fetch_env!("TELEGRAM_BOT_TOKEN")
config :aier_bot, :aier_api_token, System.fetch_env!("AIER_API_TOKEN")
config :aier_bot, :openai_api_key, System.fetch_env!("OPENAI_API_KEY")
11 changes: 11 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# fly

- https://fly.io/apps/aier-bot/monitoring

# docs

- [Tesla.Adapter.Hackney](https://hexdocs.pm/tesla/Tesla.Adapter.Hackney.html)
- [Continuous Deployment with Fly.io and GitHub Actions](https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/)
- [System.get_env()](https://hexdocs.pm/elixir/System.html#get_env/0)
- [ex_gram](https://github.com/rockneurotiko/ex_gram#configuration)
- [Github Actions for Elixir CI](https://fly.io/phoenix-files/github-actions-for-elixir-ci/)
Binary file added docs/assets/save-memo-to-aier.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions docs/dev-log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 2023-09-15

issues: fly deploy --remote-only fails with:

```log
** (EXIT) an exception was raised:
** (UndefinedFunctionError) function Tesla.Adapter.Hackney.call/2 is undefined (module Tesla.Adapter.Hackney is not available)
```

resolved by

```sh
fly deploy --remote-only --no-cache
```

docs:

- [Continuous Deployment with Fly.io and GitHub Actions · Fly Docs](https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/)
2 changes: 2 additions & 0 deletions elixir_buildpack.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
elixir_version=1.15.4
erlang_version=26.0.2
22 changes: 22 additions & 0 deletions fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# fly.toml app configuration file generated for aier-bot on 2023-09-15T13:17:27+09:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = "aier-bot"
primary_region = "nrt"

[build]
builder = "heroku/buildpacks:20"
buildpacks = ["https://cnb-shim.herokuapp.com/v1/hashnuke/elixir"]

[env]
PORT = "8080"

[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]
23 changes: 23 additions & 0 deletions lib/aier_bot/aier_api.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule AierBot.AierApi do
use Tesla

plug(Tesla.Middleware.BaseUrl, "https://www.aier.app")
plug(Tesla.Middleware.JSON)

def aier_api_token do
Application.fetch_env!(:aier_bot, :aier_api_token)
end

def create_memo(memo) do
url = Path.join("/api/webhooks/memoCreate", aier_api_token())

body = %{
content: memo,
created_at: DateTime.to_iso8601(DateTime.utc_now()),
# TODO: real source_url
source_url: "telegram bot"
}

post(url, body)
end
end
22 changes: 22 additions & 0 deletions lib/aier_bot/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule AierBot.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
token = Application.fetch_env!(:aier_bot, :telegram_bot_token)

children = [
ExGram,
{AierBot.Bot, [method: :polling, token: token]}
]

# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: AierBot.Supervisor]
Supervisor.start_link(children, opts)
end
end
62 changes: 62 additions & 0 deletions lib/aier_bot/bot.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
defmodule AierBot.Bot do
alias AierBot.CobaltClient
alias AierBot.AierApi
alias AierBot.OpenaiApi

@bot :aier_bot

use ExGram.Bot,
name: @bot,
setup_commands: true

command("start")
command("help", description: "Print the bot's help")
command("image", description: "Generate image from text")

middleware(ExGram.Middleware.IgnoreUsername)

def bot(), do: @bot

def handle({:command, :start, _msg}, context) do
answer(context, "Hi!")
end

def handle({:command, :help, _msg}, context) do
answer(context, "Here is your help:")
end

def handle({:command, :image, message}, context) do
%{text: prompt, chat: %{id: chat_id}} = message

case OpenaiApi.image_generation(prompt) do
{:ok, response} -> image_generation_success(response, chat_id)
{:error, error} -> answer(context, "Error: #{inspect(error)}")
end
end

def handle({:text, text, _msg}, context) do
# TODO: repeat
# request download API
data = CobaltClient.json(text)

IO.inspect(data)

answer(context, "#{data}")
# case AierApi.create_memo(text) do
# {:ok, response} -> create_memo_success(response, context)
# {:error, error} -> answer(context, "Error: #{inspect(error)}")
# end
end

def create_memo_success(_, context) do
answer(context, "Memo saved!")
end

def image_generation_success(%{data: images}, chat_id) do
[first | _] = images

res = ExGram.send_photo(chat_id, {:file_content, first, "image.png"}, bot: @bot)

IO.inspect(res)
end
end
29 changes: 29 additions & 0 deletions lib/aier_bot/cobalt_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule AierBot.CobaltClient do
alias AierBot.FileDownloader
use Tesla

plug(Tesla.Middleware.BaseUrl, "https://api.cobalt.tools")

plug(Tesla.Middleware.Headers, [
{"Accept", "application/json"},
{"Content-Type", "application/json"}
])

plug(Tesla.Middleware.JSON)

def json(url) do
case post("api/json", %{url: url}) do
{:ok, response} ->
# %{
# "status" => "redirect",
# "url" => "https://video.twimg.com/amplify_video/1814202798097268736/vid/avc1/720x1192/HAD9zyJn1xoP4oRN.mp4?tag=16"
# }
%{"url" => url} = response.body
FileDownloader.download(url, "video.mp4")
url

{:error, error} ->
error
end
end
end
17 changes: 17 additions & 0 deletions lib/aier_bot/file_downloader.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule AierBot.FileDownloader do
use Tesla

def download(url, file_path) do
case get(url) do
{:ok, %Tesla.Env{status: 200, body: body}} ->
File.write("./downloads/#{file_path}", body)
IO.puts("File downloaded successfully.")

{:ok, %Tesla.Env{status: status}} ->
IO.puts("Failed to download file. Status: #{status}")

{:error, reason} ->
IO.puts("Failed to download file. Reason: #{inspect(reason)}")
end
end
end
32 changes: 32 additions & 0 deletions lib/aier_bot/openai_api.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule AierBot.OpenaiApi do
alias OpenaiEx.Image

def openai() do
api_key = Application.fetch_env!(:aier_bot, :openai_api_key)
openai = OpenaiEx.new(api_key)

openai
end

def image_generation(prompt) do
openai = openai()

fetch_blob = fn url ->
Finch.build(:get, url)
|> Finch.request!(OpenaiEx.Finch)
|> Map.get(:body)
end

images =
openai
|> Image.create(%{
prompt: prompt,
n: 2,
size: "1024x1024"
})
|> Map.get("data")
|> Enum.map(fn x -> x["url"] |> fetch_blob.() end)

{:ok, %{data: images}}
end
end
32 changes: 32 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule AierBot.MixProject do
use Mix.Project

def project do
[
app: :aier_bot,
version: "0.1.0",
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger],
mod: {AierBot.Application, []}
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ex_gram, "~> 0.40.0"},
{:tesla, "~> 1.7.0"},
{:openai_ex, "~> 0.3.0"},
{:hackney, "~> 1.18.2"},
{:jason, ">= 1.4.1"}
]
end
end
Loading

0 comments on commit 7969aad

Please sign in to comment.