diff --git a/.config/ai/scpt b/.config/ai/scpt new file mode 160000 index 0000000..537b179 --- /dev/null +++ b/.config/ai/scpt @@ -0,0 +1 @@ +Subproject commit 537b17910715eba88497cd59ee651bd49766c58f diff --git a/.config/keep b/.config/keep new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d414aff --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +Cargo.lock +target +*.json +*.DS_Store +**.DS_Store +scpt/json/ +.config/ai/*.toml +.config/ai/*.json +.config/ai/txt +.config/ai/png +.ssh/*.key +.ssh/*.pub +.ssh/*config +.env diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d677070 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "bot_scpt"] + path = .config/ai/scpt + url = git@git.syui.ai:ai/bot_scpt.git diff --git a/.ssh/keep b/.ssh/keep new file mode 100644 index 0000000..e69de29 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f2a14be --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ai" +version = "0.0.1" +edition = "2021" + +[dependencies] +seahorse = "*" +reqwest = { version = "*", features = ["blocking", "json"] } +tokio = { version = "1", features = ["full"] } +shellexpand = "*" +config = "*" +serde = "*" +serde_json = "*" +serde_derive = "*" +url = { version = "2.0", features = ["serde"] } +rustc-serialize = "*" +toml = "*" +iso8601-timestamp = "*" +sysinfo = "*" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..df71fd2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM syui/aios +ADD .ssh /root/.ssh + +WORKDIR /root +ADD ./test/entrypoint.sh . +RUN chmod +x /root/entrypoint.sh + +ENTRYPOINT ["/root/entrypoint.sh"] diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 0000000..83c5c1f --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,25 @@ +[tasks.format] +install_crate = "rustfmt" +command = "cargo" +args = ["fmt", "--", "--emit=files"] + +[tasks.clean] +command = "cargo" +args = ["clean"] + +[tasks.build] +command = "cargo" +args = ["build"] +dependencies = ["clean"] + +[tasks.test] +command = "cargo" +args = ["test"] +dependencies = ["clean"] + +[tasks.my-flow] +dependencies = [ + "format", + "build", + "test" +] diff --git a/README.md b/README.md index c7b3451..c733ce7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,102 @@ ## ai `bot` + + +- name : ai bot +- base : [ai os](https://git.syui.ai/ai/os) +- host : [yui.syui.ai](https://bsky.app/profile/yui.syui.ai), [ai.syu.is](https://web.syu.is/profile/ai.syu.is) + +```sh +$ ai +``` + +### logo + ```sh $ cargo build -$ ./target/debug/ai +$ ./target/debug/ai ai +``` + +```sh +$ ai ai -t avatar +``` + +### login + +```sh +# ai login $handle -p $password +$ ai l yui.syui.ai -p password + +$ cat ~/.config/ai/token.toml +``` + +```sh +# ai l $handle -p $password -s $server +$ ai l ai.syu.is -p password -s syu.is +``` + +### refresh + +``` +$ ai r +``` + +### notify + +``` +$ ai n +``` + +``` +$ ai n | jq . +``` + +### bot + +``` +$ ai bot +``` + +|command|sub|type|link|auth| +|---|---|---|---|---| +|@yui.syui.ai did||mention, reply| [plc.directory](https://plc.directory)/$did/log |user| +|@yui.syui.ai card|r, s, b|mention, reply| [card.syui.ai](https://card.syui.ai) |user| +|@yui.syui.ai ten|start, d, p, coin|mention, reply| [card.syui.ai](https://card.syui.ai) |user| +|@yui.syui.ai fav|{cid}|mention, reply| [card.syui.ai](https://card.syui.ai) |user| +|@yui.syui.ai egg|{password}|mention, reply| [card.syui.ai](https://card.syui.ai) |user| +|@yui.syui.ai 占い||mention, reply| [yui.syui.ai](https://yui.syui.ai) |user| +|@yui.syui.ai nyan|🍰|mention, reply| [yui.syui.ai](https://yui.syui.ai) |user| +|@yui.syui.ai diffusers|{keyword}|mention, reply| [huggingface.co/diffusers](https://huggingface.co/docs/diffusers/index) |user| +|@yui.syui.ai sh|{command}|mention, reply| [archlinux.org](https://wiki.archlinux.org/title/Systemd-nspawn) |admin| + +### test + +`zsh` + +```sh +$ ./test/ai.zsh t +``` + +### make + +```sh +$ cargo install --force cargo-make +$ cargo make build +``` + + +### docker + +> .env + +```sh +HANDLE=ai.syu.is +PASSWORD=xxx +HOST=syu.is +ADMIN=syui.syu.is +``` + +```sh +$ docker compose build +$ docker compose up -d ``` diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..2d6c159 --- /dev/null +++ b/compose.yml @@ -0,0 +1,11 @@ +services: + aios: + #image: syui/aios + #command: ai bot -a syui.syu.is + build: + context: . + restart: always + env_file: + - .env + volumes: + - ./.config:/root/.config diff --git a/docs/wiki.md b/docs/wiki.md new file mode 100644 index 0000000..cd2152b --- /dev/null +++ b/docs/wiki.md @@ -0,0 +1,52 @@ +### docker + +```sh +$ docker run -it syui/aios ai +$ docker run -it -d syui/aios zsh -c "ai login -p && ai bot" +``` + +```sh +$ cp -rf ~/.config/ai ./.config/ + +$ docker compose up +``` + +### cron + +```sh +$ sudo pacman -S fcron +$ fcrontab -e +* * * * * $HOME/bot/test/ai.zsh c +``` + +### ssh + +```sh +$ ssh-keygen -f /.ssh/diffusers.key -t ed25519 +``` + +```sh +FROM syui/aios +ADD .ssh /root/.ssh +``` + +```sh +Host diffusers + HostName localhost + User root + IdentityFile ~/.ssh/diffusers.key + StrictHostKeyChecking no + UserKnownHostsFile /dev/null +``` + +```sh +services: + aios: + #image: syui/aios + build: + context: . + restart: always + volumes: + - ./.config:/root/.config + command: ai bot -a syui.syu.is +``` diff --git a/icon/ai.png b/icon/ai.png new file mode 100644 index 0000000..d21dabe Binary files /dev/null and b/icon/ai.png differ diff --git a/icon/avatar.png b/icon/avatar.png new file mode 100644 index 0000000..f091ef1 Binary files /dev/null and b/icon/avatar.png differ diff --git a/icon/bot_scpt.png b/icon/bot_scpt.png new file mode 100644 index 0000000..8ad5c8d Binary files /dev/null and b/icon/bot_scpt.png differ diff --git a/k8s/aios-deployment.yaml b/k8s/aios-deployment.yaml new file mode 100644 index 0000000..e2cdf38 --- /dev/null +++ b/k8s/aios-deployment.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.cmd: kompose convert -f ../compose.yml --volumes hostPath + kompose.version: 1.32.0 (HEAD) + labels: + io.kompose.service: aios + name: aios +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: aios + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.cmd: kompose convert -f ../compose.yml --volumes hostPath + kompose.version: 1.32.0 (HEAD) + labels: + io.kompose.network/bot-default: "true" + io.kompose.service: aios + spec: + containers: + - env: + - name: ADMIN + valueFrom: + configMapKeyRef: + key: ADMIN + name: env + - name: HANDLE + valueFrom: + configMapKeyRef: + key: HANDLE + name: env + - name: HOST + valueFrom: + configMapKeyRef: + key: HOST + name: env + - name: PASSWORD + valueFrom: + configMapKeyRef: + key: PASSWORD + name: env + image: aios + name: aios + volumeMounts: + - mountPath: /root/.config + name: aios-hostpath0 + restartPolicy: Always + volumes: + - hostPath: + path: /Users/syui/ai/bot/.config + name: aios-hostpath0 diff --git a/k8s/env-configmap.yaml b/k8s/env-configmap.yaml new file mode 100644 index 0000000..521649b --- /dev/null +++ b/k8s/env-configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +data: + ADMIN: $ADMIN + HANDLE: $HANDLE + HOST: $HOST + PASSWORD: $PASSWORD +kind: ConfigMap +metadata: + labels: + io.kompose.service: aios-env + name: env diff --git a/src/ascii.rs b/src/ascii.rs new file mode 100644 index 0000000..7d7fd46 --- /dev/null +++ b/src/ascii.rs @@ -0,0 +1,205 @@ +use sysinfo::System; + +pub fn c_ascii(x: &str) { + let logo = " +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠋⠉⠀⠀⠀⠀⠀⠈⠉⠛⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿⣿⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣤⣤⣤⣤⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⠀⠀⠀⠀ +⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀ +⠀⠀⠀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀ +⠀⠀⠘⠛⠛⠛⠛⠉⠉⠉⠉⠁⠀⠀⠀⠀⠈⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋⠀⠀⠀⠀⠀⠉⠉⠉⠉⠙⠛⠛⠛⠛⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠛⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠟⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +"; + + let avatar = " +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣁⣀⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣤⠖⠋⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠙⠒⢦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⢿⣷⣦⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣴⣿⠷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⣶⠿⠟⠛⠛⠋⠉⠋⠉⠉⠉⠉⠙⠛⠙⠛⠛⠻⠿⢷⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠚⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⣼⡀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢠⣷⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⣼⣿⡇⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢁⣾⣿⡆⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢰⣿⣿⣧⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⣾⣿⣿⡇⣿⣿⣿⢻⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⣾⣿⣿⣿⡄⣿⣿⣿⣿⣿⣿⣿⣿⠏⣼⣿⣿⣿⡇⣿⡿⢣⠈⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⢹⠀⣿⣿⣿⣿⣧⢹⣿⡏⢹⣿⣿⣿⠏⣼⣿⡿⠿⢿⢣⡿⣡⣿⠀⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⢃⠸⠀⣵⡶⠶⢶⣿⡆⢿⢁⠸⣿⣿⢋⣼⣯⡴⠒⠒⠒⣼⡑⢟⣿⢠⣿⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⢿⣿⣿⣿⣿⡟⠈⣄⠈⠡⡐⢿⣦⠹⣿⡌⢸⡆⠿⣡⣾⣿⡟⢐⠇⠙⣡⡀⣿⡎⢿⠈⢐⡲⣆⢻⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢳⡘⣿⣿⣿⣿⡇⣸⣿⡄⣶⡄⠀⣿⠀⣿⣿⣜⣷⣼⣿⣿⣿⡇⢸⡇⠀⣿⠃⣿⣿⣸⠀⡌⢇⣿⠘⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⠘⣿⣿⣿⡇⣿⣿⣧⡘⢷⣾⠟⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣌⣿⣿⣃⣼⣿⣿⡟⠀⣿⣾⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣿⡇⢸⣿⣿⣿⣾⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠡⠾⢛⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢿⡘⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢃⣶⡾⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡅⢀⡹⣿⣿⣿⣿⣿⣿⣯⣛⡛⣛⣛⣭⣿⣿⣿⣿⣿⠿⢋⣤⠟⢉⠠⣶⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠃⣸⣿⣦⡍⠛⠻⠿⣿⣿⣿⣿⣿⣿⣿⠿⠿⢛⡉⢠⡶⠋⡄⢺⣿⡀⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠀⣿⣿⢿⠇⣼⢹⡀⣶⣦⣭⣭⣭⣴⣶⣿⠂⡟⠀⢋⣴⡇⡇⣾⣿⡇⢻⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⣿⡿⢸⢀⠇⣸⡀⣿⣿⣿⣿⣿⣿⣿⣿⠀⣠⠀⣿⣿⡇⠀⣿⢹⣷⠸⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⠀⣿⡇⡟⠘⢠⡿⠃⣿⣿⣿⣿⣿⣿⣿⣿⡀⣤⠀⣿⣿⠃⢰⣿⠸⣿⡀⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢜⣽⠀⣿⠁⡇⡰⣢⣼⣰⣿⣿⣿⣿⣿⣿⣿⣿⣷⣌⠀⣿⣿⠀⣼⣿⡄⣿⣧⠸⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⣠⠞⣋⠀⣿⢸⠃⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⣿⣿⢠⣬⣍⣁⣙⠛⠦⠹⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⢋⠜⣡⣿⣿⢠⡿⠘⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢠⣿⡇⣸⣿⣿⣿⣿⣿⣷⣦⣍⠻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠞⡰⢫⣾⣿⡟⣵⢸⡇⠀⢸⡿⠿⢛⣋⣉⣿⣿⣿⣿⣿⣿⣫⣭⡍⢸⣿⠇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡌⢦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡴⢃⡞⣰⣿⣿⣿⣦⡛⢸⡇⠀⣿⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⢸⣿⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡈⢷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡞⢡⡏⣼⣿⣿⣿⣿⣿⣿⢸⡇⠀⣭⣭⣭⣤⣶⣦⢠⣭⣙⠻⣿⣿⣿⣿⡇⣾⡇⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⡄⢻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡟⢰⣿⢰⣿⣿⣿⣿⣿⣿⣿⠸⣇⠀⣿⣿⣿⣿⣿⡇⢸⣿⣿⣿⡜⣿⣿⣿⡇⣿⠀⢸⣿⡿⠿⠿⠛⠿⠿⠟⣛⣛⣉⣥⣿⡈⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + +"; + + let color = " +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣾⣿⣿⣿⣿⣿⣿⣧⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⡿⠟⠛⠉⠉⠉⠙⠛⠿⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣿⣿⣿⣿⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣄⣀⣀⣠⣤⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠟⠛⠛⠛⠛⠉⠉⠉⠙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠉⠉⠉⠙⠛⠛⠛⠛⠿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠛⠻⠿⠿⠿⠿⠟⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + "; + + let avatar_color = " +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⡀⠀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡤⠖⠊⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠁⠒⠦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠿⣷⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣴⡿⠷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⡶⠾⠟⠛⠋⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠛⠛⠿⠷⣶⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠊⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠉⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠋⣴⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⢀⣧⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⣸⣿⡆⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⣾⣿⡀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⢠⣿⣿⣧⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⣼⣿⣿⡇⣿⣿⡿⠛⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣿⣿⣿⣿⣿⣿⣿⣿⠃⣼⣿⣿⣿⡀⣿⣿⣿⣿⣿⣿⣿⣿⠃⣰⣿⣿⣿⠇⣿⡿⢡⠀⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⡏⢸⠀⣿⡿⢿⣿⣇⠸⣿⠏⢹⣿⣿⣿⠃⣰⣿⠿⠿⠿⢀⡟⢡⣿⠀⣿⣿⣿⣿⣿⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⠃⠘⠀⣤⠶⠶⢦⣿⡆⢻⠀⠘⣿⣿⠃⣴⣯⡴⠒⠒⠂⠜⡀⠛⣿⠀⣿⠿⢿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⢻⣿⣿⣿⣿⡏⠈⡀⠀⠡⠀⠶⣆⠹⣿⡌⢸⡄⠿⢡⣾⣿⡏⠐⠂⠘⣁⠀⣿⡄⢿⠀⠐⠒⡆⢹⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠳⠈⣿⣿⣿⣿⡇⢰⣿⡀⢰⠀⠀⣹⠀⣿⣷⣌⣷⣴⣿⣿⣿⡇⢸⡀⠀⣸⠀⣿⣷⣸⠀⡌⠃⡿⠘⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠈⢿⣿⣿⡇⢹⣿⣧⠘⢷⢶⠋⣰⣿⣿⣿⣿⣿⣿⣿⣿⣷⣌⡛⠚⢁⣼⣿⣿⡏⠀⣿⣾⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⣿⡇⢸⣿⣿⣿⣾⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠠⠞⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢷⡈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡁⢀⠙⢿⣿⣿⣿⣿⣿⣍⣛⠛⢛⣋⣩⣿⣿⣿⣿⣿⠿⠋⠀⠀⢀⠀⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⣸⣷⣤⡉⠛⠻⠿⣿⣿⣿⣿⣿⣿⡿⠿⠟⠛⠉⠀⠀⠀⡀⢰⣿⠀⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⣿⣿⢻⠃⠈⠀⠀⣶⣤⣤⣭⣭⣴⣶⣾⠀⠌⠀⢀⣴⡆⠃⣸⢿⡆⢸⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⣿⡏⢸⠀⠃⠐⠀⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⣿⣿⠁⠀⡇⢸⣇⠘⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⠀⣿⠇⡄⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⠀⣄⠀⣿⣿⠀⢀⣧⠈⣿⠀⢿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣹⠀⣿⠀⡇⠠⢀⣴⣠⣿⣿⣿⣿⣿⣿⣿⣿⣷⣌⠀⣿⣿⠀⣸⣿⠀⢿⣇⠘⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠄⡠⠞⣋⠀⡿⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⣿⡟⢠⣤⣈⣁⡈⠛⠂⠹⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⢂⠔⣡⣾⡯⠀⡇⠀⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⣿⡇⢸⣿⣿⣿⣿⣿⣶⣦⣈⠛⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⡠⢃⣼⣿⠟⣰⠀⡇⠀⢸⡿⠿⠛⣋⣉⣻⣿⣿⣿⣿⡿⢋⣤⡍⢰⣿⠃⣾⣿⣿⣿⣿⣿⣿⣿⣿⣷⡌⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⢁⠜⣰⣿⣿⣿⣤⡙⠘⡇⠀⣼⣷⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⢸⡿⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠈⢧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡜⢠⠎⣰⣿⣿⣿⣿⣿⣿⠀⡇⠀⣉⣩⣥⣤⣤⡄⢠⣍⣙⠻⢿⣿⣿⣿⡇⢸⠇⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⡄⢳⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡞⢠⣿⢠⣿⣿⣿⣿⣿⣿⣿⠀⡇⠀⣿⣿⣿⣿⣿⡇⢸⣿⣿⣿⡘⣿⣿⣿⠃⡿⠀⢸⣿⡿⠿⠛⠛⠛⠿⠛⢛⣋⣉⣀⣿⠀⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + "; + + let mut sys = System::new_all(); + sys.refresh_all(); + + let s = x.to_string(); + match &*s { + "logo" => println!("{}", logo), + "color" => println!("{}", color), + "avatar" => println!("{}", avatar), + "avatar_color" => println!("{}", avatar_color), + "os" => println!("{}\n\t", color), + _ => println!("not matched"), + } + + println!("total memory: {} bytes", sys.total_memory()); + println!("System name: {:?}", System::name().unwrap()); + println!( + "System kernel version: {:?}", + System::kernel_version().unwrap() + ); + //println!("System OS version: {:?}", System::os_version().unwrap()); + println!( + "System host name: {:?}", + System::host_name().unwrap() + ); + println!("NB CPUs: {}", sys.cpus().len()); + + //let disks = Disks::new_with_refreshed_list(); + //for disk in &disks { + // println!("{disk:?}"); + //} + + //let networks = Networks::new_with_refreshed_list(); + //println!("=> networks:"); + //for (interface_name, data) in &networks { + // println!("{interface_name}: {}/{} B", data.received(), data.transmitted()); + //} + + //let components = Components::new_with_refreshed_list(); + //println!("=> components:"); + //for component in &components { + // println!("{component:?}"); + //} +} diff --git a/src/bot.rs b/src/bot.rs new file mode 100644 index 0000000..5e3865d --- /dev/null +++ b/src/bot.rs @@ -0,0 +1,460 @@ +use seahorse::Context; +use std::process::Command; + +use crate::notify; +use crate::notify_read; +use crate::openai; +use crate::refresh; +use crate::reply; +use crate::reply_link; + +use crate::data::c_char; +use crate::data::data_scpt; +use crate::data::log_file; +use crate::data::w_cid; +use crate::data::Notify; + +pub fn c_bot(c: &Context) { + let h = async { + let mut notify = notify::get_request(100).await; + if notify == "err" { + refresh(c); + notify = notify::get_request(100).await; + } + let notify: Notify = serde_json::from_str(¬ify).unwrap(); + + let n = notify.notifications; + let length = &n.len(); + let su = 0..*length; + for i in su { + let reason = &n[i].reason; + let handle = &n[i].author.handle; + let did = &n[i].author.did; + let read = n[i].isRead; + let cid = &n[i].cid; + let uri = &n[i].uri; + let time = &n[i].indexedAt; + let mut cid_root = cid; + let mut uri_root = uri; + let check_cid = w_cid(cid.to_string(), log_file(&"n1"), false); + let check_cid_run = w_cid(cid.to_string(), log_file(&"n2"), false); + // thread + if !n[i].record.reply.is_none() { + cid_root = &n[i].record.reply.as_ref().unwrap().root.cid; + uri_root = &n[i].record.reply.as_ref().unwrap().root.uri; + } + + let mut text = ""; + if !n[i].record.text.is_none() { + text = &n[i].record.text.as_ref().unwrap(); + } + let vec: Vec<&str> = text.split_whitespace().collect(); + let handlev: Vec<&str> = handle.split('.').collect(); + let mut handlev = handlev[0].trim().to_string(); + + let mut link = "https://card.syui.ai/".to_owned() + &handlev; + let s = 0; + let mut e = link.chars().count(); + + let mut com = "".to_string(); + let mut prompt = "".to_string(); + let mut prompt_sub = "".to_string(); + let mut prompt_chat = "".to_string(); + if reason == "mention" { + com = vec[1].trim().to_string(); + prompt = vec[2..].join(" "); + prompt_chat = vec[1..].join(" "); + if vec.len() > 2 { + prompt_sub = vec[3..].join(" "); + } + } else if reason == "reply" { + com = vec[0].trim().to_string(); + prompt = vec[1..].join(" "); + prompt_chat = vec[0..].join(" "); + if vec.len() > 1 { + prompt_sub = vec[2..].join(" "); + } + } + + if prompt.is_empty() == false || com.is_empty() == false { + println!("{}", read); + println!("{}", handle); + println!( + "cid:{}\nuri:{}\ncid_root:{}\nuri_root:{}", + cid, uri, cid_root, uri_root + ); + println!("reason:{}\ncom:{}\nprompt:{}", reason, com, prompt); + println!("prompt_sub:{}", prompt_sub); + } + + let mut admin = "".to_string(); + if c.string_flag("admin").is_ok() { + admin = c.string_flag("admin").unwrap(); + } + + if check_cid == false && { reason == "mention" || reason == "reply" } + || check_cid_run == false && { reason == "mention" || reason == "reply" } + { + w_cid(cid.to_string(), log_file(&"n2"), true); + if com == "did" { + let link = "https://plc.directory/".to_owned() + &did + &"/log"; + let s = 0; + let e = link.chars().count(); + let d = "\n".to_owned() + &did.to_string(); + let text_limit = c_char(d); + if text_limit.len() > 3 { + let str_rep = reply_link::post_request( + text_limit.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + w_cid(cid.to_string(), log_file(&"n1"), true); + println!("{}", str_rep); + } + } else if com == "help" { + let link = "https://git.syui.ai/ai/bot/wiki/help".to_string(); + let s = 0; + let e = link.chars().count(); + let str_rep = reply_link::post_request( + "\n".to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + w_cid(cid.to_string(), log_file(&"n1"), true); + println!("{}", str_rep); + } else if com == "diffusers" { + let _output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"diffusers") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + w_cid(cid.to_string(), log_file(&"n1"), true); + } else if com.contains("占") == true + || com.contains("うらない") == true + || com.contains("うらなって") == true + { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"fortune") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let d = d.to_string(); + let text_limit = c_char(d); + if text_limit.len() > 3 { + println!("{}", text_limit); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else if com == "card" { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"card") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let dd = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(dd); + if text_limit.len() > 3 { + if d.contains("handle") == false { + let str_rep = reply_link::post_request( + text_limit.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + } else { + handlev = handle.replace(".", "-").to_string(); + link = "https://card.syui.ai/".to_owned() + &handlev; + e = link.chars().count(); + let str_rep = reply_link::post_request( + d.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + } + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else if com == "fav" { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"fav") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let dd = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(dd); + if text_limit.len() > 3 { + let str_rep = reply_link::post_request( + d.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else if com == "egg" { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"egg") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let dd = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(dd); + if text_limit.len() > 3 { + let str_rep = reply_link::post_request( + d.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else if com == "nyan" { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"nyan") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let dd = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(dd); + println!("{}", text_limit); + if text_limit.len() > 3 { + let str_rep = reply::post_request( + text_limit.to_string(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else if com == "ten" { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"ten") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let d = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(d); + if text_limit.len() > 3 { + let str_rep = reply_link::post_request( + text_limit.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else if com == "coin" { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"coin") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let d = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(d); + if text_limit.len() > 3 { + let str_rep = reply_link::post_request( + text_limit.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else if com == "sh" && handle == &admin { + println!("admin:{}", admin); + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"sh") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let d = d.to_string(); + let text_limit = c_char(d); + let str_rep = reply::post_request( + text_limit.to_string(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } else if com == "mitractl" && handle == &admin { + println!("admin:{}", admin); + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"mitra") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let d = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(d); + link = "https://m.syu.is".to_string(); + e = link.chars().count(); + if text_limit.len() > 3 { + let str_rep = reply_link::post_request( + text_limit.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else { + // openai + let str_openai = openai::post_request(prompt_chat.to_string()).await; + let text_limit = c_char(str_openai); + let str_rep = reply::post_request( + text_limit.to_string(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + let str_notify = notify_read::post_request(time.to_string()).await; + println!("{}", str_notify); + println!("---"); + } + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 0000000..3f569e6 --- /dev/null +++ b/src/data.rs @@ -0,0 +1,647 @@ +use config::{Config, ConfigError, File}; +use seahorse::Context; +use serde_derive::{Deserialize, Serialize}; +use std::fs; +use std::fs::OpenOptions; +use std::io::Read; +use std::io::Write; +use std::path::Path; + +pub fn data_file(s: &str) -> String { + let file = "/.config/ai/"; + let mut f = shellexpand::tilde("~").to_string(); + f.push_str(&file); + let path = Path::new(&f); + if path.is_dir() == false { + let _ = fs::create_dir_all(f.clone()); + } + match &*s { + "toml" => f + &"token.toml", + "json" => f + &"token.json", + "refresh" => f + &"refresh.toml", + _ => f + &"." + &s, + } +} + +pub fn log_file(s: &str) -> String { + let file = "/.config/ai/txt/"; + let mut f = shellexpand::tilde("~").to_string(); + f.push_str(&file); + let path = Path::new(&f); + if path.is_dir() == false { + let _ = fs::create_dir_all(f.clone()); + } + match &*s { + "n1" => f + &"notify_cid.txt", + "n2" => f + &"notify_cid_run.txt", + _ => f + &s, + } +} + +impl Token { + pub fn new() -> Result { + let d = data_file("json"); + let s = Config::builder() + .add_source(File::with_name(&d)) + .add_source(config::Environment::with_prefix("APP")) + .build()?; + s.try_deserialize() + } +} + +impl Data { + pub fn new() -> Result { + let d = data_file("toml"); + let s = Config::builder() + .add_source(File::with_name(&d)) + .add_source(config::Environment::with_prefix("APP")) + .build()?; + s.try_deserialize() + } +} + +impl Refresh { + pub fn new() -> Result { + let d = data_file("refresh"); + let s = Config::builder() + .add_source(File::with_name(&d)) + .add_source(config::Environment::with_prefix("APP")) + .build()?; + s.try_deserialize() + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Token { + pub did: String, + pub handle: String, + pub accessJwt: String, + pub refreshJwt: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Data { + pub host: String, + pub password: String, + pub did: String, + pub handle: String, + pub access: String, + pub refresh: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Refresh { + pub access: String, + pub refresh: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BaseUrl { + pub profile_get: String, + pub thread_get: String, + pub describe: String, + pub record_list: String, + pub record_create: String, + pub record_delete: String, + pub session_create: String, + pub session_refresh: String, + pub session_get: String, + pub timeline_get: String, + pub timeline_author: String, + pub upload_blob: String, + pub update_handle: String, + pub account_create: String, + pub notify_count: String, + pub notify_list: String, + pub notify_update: String, + pub repo_update: String, + pub like: String, + pub repost: String, + pub follow: String, + pub follows: String, + pub followers: String, +} + +pub fn url(s: &str) -> String { + let s = String::from(s); + let data = Data::new().unwrap(); + let data = Data { + host: data.host, + password: data.password, + handle: data.handle, + did: data.did, + access: data.access, + refresh: data.refresh, + }; + let t = "https://".to_string() + &data.host.to_string() + &"/xrpc/".to_string(); + let baseurl = BaseUrl { + profile_get: "com.atproto.identity.resolveHandle".to_string(), + thread_get: "app.bsky.feed.getPostThread".to_string(), + record_create: "com.atproto.repo.createRecord".to_string(), + record_delete: "com.atproto.repo.deleteRecord".to_string(), + describe: "com.atproto.repo.describeRepo".to_string(), + record_list: "com.atproto.repo.listRecords".to_string(), + session_create: "com.atproto.server.createSession".to_string(), + session_refresh: "com.atproto.server.refreshSession".to_string(), + session_get: "com.atproto.server.getSession".to_string(), + timeline_get: "app.bsky.feed.getTimeline".to_string(), + timeline_author: "app.bsky.feed.getAuthorFeed".to_string(), + like: "app.bsky.feed.like".to_string(), + repost: "app.bsky.feed.repost".to_string(), + follow: "app.bsky.graph.follow".to_string(), + follows: "app.bsky.graph.getFollows".to_string(), + followers: "app.bsky.graph.getFollowers".to_string(), + upload_blob: "com.atproto.repo.uploadBlob".to_string(), + account_create: "com.atproto.server.createAccount".to_string(), + update_handle: "com.atproto.identity.updateHandle".to_string(), + notify_count: "app.bsky.notification.getUnreadCount".to_string(), + notify_list: "app.bsky.notification.listNotifications".to_string(), + notify_update: "app.bsky.notification.updateSeen".to_string(), + repo_update: "com.atproto.sync.updateRepo".to_string(), + }; + + match &*s { + "profile_get" => t.to_string() + &baseurl.profile_get, + "thread_get" => t.to_string() + &baseurl.thread_get, + "describe" => t.to_string() + &baseurl.describe, + "record_list" => t.to_string() + &baseurl.record_list, + "record_create" => t.to_string() + &baseurl.record_create, + "record_delete" => t.to_string() + &baseurl.record_delete, + "session_create" => t.to_string() + &baseurl.session_create, + "session_refresh" => t.to_string() + &baseurl.session_refresh, + "session_get" => t.to_string() + &baseurl.session_get, + "timeline_get" => t.to_string() + &baseurl.timeline_get, + "timeline_author" => t.to_string() + &baseurl.timeline_get, + "upload_blob" => t.to_string() + &baseurl.upload_blob, + "account_create" => t.to_string() + &baseurl.account_create, + "update_handle" => t.to_string() + &baseurl.update_handle, + "notify_list" => t.to_string() + &baseurl.notify_list, + "notify_count" => t.to_string() + &baseurl.notify_count, + "notify_update" => t.to_string() + &baseurl.notify_update, + "repo_update" => t.to_string() + &baseurl.repo_update, + "like" => t.to_string() + &baseurl.like, + "repost" => t.to_string() + &baseurl.repost, + "follow" => t.to_string() + &baseurl.follow, + "follows" => t.to_string() + &baseurl.follows, + "followers" => t.to_string() + &baseurl.followers, + _ => s, + } +} + +pub fn data_toml(s: &str) -> String { + let s = String::from(s); + let data = Data::new().unwrap(); + let data = Data { + host: data.host, + password: data.password, + handle: data.handle, + did: data.did, + access: data.access, + refresh: data.refresh, + }; + match &*s { + "host" => data.host, + "password" => data.password, + "handle" => data.handle, + "did" => data.did, + "access" => data.access, + "refresh" => data.refresh, + _ => s, + } +} + +pub fn c_refresh(access: &str, refresh: &str) { + let ff = data_file(&"refresh"); + let mut ff = fs::File::create(ff.clone()).unwrap(); + let refreshs = Refresh { + access: access.to_string(), + refresh: refresh.to_string(), + }; + let toml = toml::to_string(&refreshs).unwrap(); + ff.write_all(&toml.as_bytes()).unwrap(); +} + +pub fn data_refresh(s: &str) -> String { + let s = String::from(s); + + let data = Data::new().unwrap(); + let data = Data { + host: data.host, + password: data.password, + handle: data.handle, + did: data.did, + access: data.access, + refresh: data.refresh, + }; + + let mut _file = match Refresh::new() + { + Err(_why) => c_refresh(&data.access, &data.refresh), + Ok(_) => println!(""), + }; + let refresh = Refresh::new().unwrap(); + let refresh = Refresh { + access: refresh.access, + refresh: refresh.refresh, + }; + match &*s { + "access" => refresh.access, + "refresh" => refresh.refresh, + _ => s, + } +} + +pub fn data_scpt(s: &str) -> String { + let s = String::from(s); + let file = "/.config/ai/scpt/".to_owned() + &s + &".zsh"; + let mut f = shellexpand::tilde("~").to_string(); + f.push_str(&file); + return f; +} + +#[derive(Serialize, Deserialize)] +pub struct Notify { + pub notifications: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct Status { + pub handle: String, + pub did: String, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct DidDocs { + pub verificationMethod: Vec, + pub service: Vec, + pub id: String, + pub alsoKnownAs: Vec, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct VerificationMethod { + pub id: String, + pub r#type: String, + pub controller: String, + pub publicKeyMultibase: String, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Service { + pub id: String, + pub r#type: String, + pub serviceEndpoint: String, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct AlsoKnownAs {} + +#[derive(Serialize, Deserialize)] +pub struct Timeline { + pub feed: Vec, +} +#[derive(Serialize, Deserialize)] +pub struct Session { + pub did: String, + pub email: String, + pub handle: String, +} +#[derive(Serialize, Deserialize)] +pub struct Follow { + pub follows: Vec, + pub cursor: String, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Notifications { + pub uri: String, + pub cid: String, + pub author: Author, + pub reason: String, + //pub reasonSubject: String, + pub record: Record, + pub isRead: bool, + pub indexedAt: String, + //pub labels: Labels, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Thread { + pub r#type: String, + pub post: String, + pub root: String, + pub author: Author, + pub reason: String, + //pub reasonSubject: String, + pub record: Record, + pub isRead: bool, + pub indexedAt: String, + //pub labels: Labels, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Author { + pub did: String, + //pub declaration: Declaration, + pub description: Option, + pub displayName: Option, + pub handle: String, + pub avatar: Option, + pub viewer: Viewer, + //pub labels: Labels, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Labels { + pub src: Option, + pub uri: Option, + pub cid: Option, + pub val: Option, + pub cts: Option, + pub neg: Option, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Declaration { + pub actorType: String, + pub cid: String, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Viewer { + pub muted: bool, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +#[derive(Debug)] +pub struct Record { + pub text: Option, + pub createdAt: String, + pub reply: Option, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +#[derive(Debug)] +pub struct Reply { + pub parent: ReplyParent, + pub root: ReplyRoot, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +#[derive(Debug)] +pub struct ReplyRoot { + pub cid: String, + pub uri: String, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +#[derive(Debug)] +pub struct ReplyParent { + pub cid: String, + pub uri: String, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Langs {} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Feed { + pub post: Post, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Post { + pub did: Option, + pub uri: String, + pub cid: String, + pub collection: Option, + pub record: Record, + pub author: Author, + pub reason: Option, + pub indexedAt: String, + pub replyCount: i32, + pub postCount: Option, + pub repostCount: i32, + pub likeCount: i32, +} + +#[derive(Serialize, Deserialize)] +pub struct Cid { + pub cid: String, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Img { + pub blob: Blob, +} + +#[derive(Serialize, Deserialize)] +pub struct Blob { + pub r#ref: Ref, +} + +#[derive(Serialize, Deserialize)] +pub struct Ref { + pub link: String, +} + +#[derive(Serialize, Deserialize)] +pub struct Handle { + pub handle: String, +} + +//#[derive(Serialize, Deserialize)] +//pub struct Did { +// pub did: String +//} + +//#[derive(Serialize, Deserialize)] +//pub struct Labels { +//} +// +//#[derive(Serialize, Deserialize)] +//pub struct Viewer { +// pub muted: bool, +// pub blockedBy: bool, +//} +// +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct ProfileIdentityResolve { + pub did: String, +} + +#[derive(Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct Profile { + pub did: String, + pub handle: String, + pub followsCount: Option, + pub followersCount: Option, + pub postsCount: i32, + pub indexedAt: Option, + pub avatar: Option, + pub banner: Option, + pub displayName: Option, + pub description: Option, + pub viewer: Viewer, + pub labels: Labels, +} + +pub fn c_char(i: String) -> String { + let l = 250; + let mut s = String::new(); + for ii in i.chars().enumerate() { + match ii.0 { + n if n > l.try_into().unwrap() => break, + _ => s.push(ii.1), + } + } + return s; +} + +pub fn w_cfg(h: &str, res: &str, password: &str) { + let f = data_file(&"json"); + let ff = data_file(&"toml"); + let mut f = fs::File::create(f.clone()).unwrap(); + let mut ff = fs::File::create(ff.clone()).unwrap(); + f.write_all(&res.as_bytes()).unwrap(); + let json: Token = serde_json::from_str(&res).unwrap(); + let datas = Data { + host: h.to_string(), + password: password.to_string(), + did: json.did.to_string(), + handle: json.handle.to_string(), + access: json.accessJwt.to_string(), + refresh: json.refreshJwt.to_string(), + }; + let toml = toml::to_string(&datas).unwrap(); + ff.write_all(&toml.as_bytes()).unwrap(); + + let ff = data_file(&"refresh"); + let mut ff = fs::File::create(ff.clone()).unwrap(); + let refreshs = Refresh { + access: json.accessJwt.to_string(), + refresh: json.refreshJwt.to_string(), + }; + let toml = toml::to_string(&refreshs).unwrap(); + ff.write_all(&toml.as_bytes()).unwrap(); +} + +pub fn w_refresh(res: &str) { + let ff = data_file(&"refresh"); + let mut ff = fs::File::create(ff.clone()).unwrap(); + let json: Token = serde_json::from_str(&res).unwrap(); + let refreshs = Refresh { + access: json.accessJwt.to_string(), + refresh: json.refreshJwt.to_string(), + }; + let toml = toml::to_string(&refreshs).unwrap(); + ff.write_all(&toml.as_bytes()).unwrap(); +} + +pub fn w_cid(cid: String, file: String, t: bool) -> bool { + let f = file; + let mut file = match OpenOptions::new() + .create(true) + .write(true) + .read(true) + .append(true) + .open(f.clone()) + { + Err(why) => panic!("Couldn't open {}: {}", f, why), + Ok(file) => file, + }; + let mut contents = String::new(); + + match file.read_to_string(&mut contents) { + Err(why) => panic!("Couldn't read {}: {}", f, why), + Ok(_) => (), + } + if contents.contains(&cid) == false { + if t { + let cid = cid + "\n"; + match file.write_all(cid.as_bytes()) { + Err(why) => panic!("Couldn't write \"{}\" to {}: {}", contents, f, why), + Ok(_) => println!("finished"), + } + } + let check = false; + return check; + } else { + let check = true; + return check; + } +} + +pub fn c_follow_all() { + let file = "/.config/ai/scpt/follow_all.zsh"; + let mut f = shellexpand::tilde("~").to_string(); + f.push_str(&file); + use std::process::Command; + let output = Command::new(&f).output().expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let d = "\n".to_owned() + &d.to_string(); + println!("{}", d); +} + +pub fn c_openai_key(c: &Context) { + let api = c.args[0].to_string(); + let o = "api='".to_owned() + &api.to_string() + &"'".to_owned(); + let o = o.to_string(); + let l = shellexpand::tilde("~") + "/.config/ai/openai.toml"; + let l = l.to_string(); + let mut l = fs::File::create(l).unwrap(); + if o != "" { + l.write_all(&o.as_bytes()).unwrap(); + } + println!("{:#?}", l); +} + +impl Open { + pub fn new() -> Result { + let d = shellexpand::tilde("~") + "/.config/ai/openai.toml"; + let s = Config::builder() + .add_source(File::with_name(&d)) + .add_source(config::Environment::with_prefix("APP")) + .build()?; + s.try_deserialize() + } +} + +#[derive(Debug, Deserialize)] +pub struct Open { + pub api: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct OpenData { + choices: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Choices { + text: String, +} + diff --git a/src/describe.rs b/src/describe.rs new file mode 100644 index 0000000..75e5ae5 --- /dev/null +++ b/src/describe.rs @@ -0,0 +1,22 @@ +extern crate reqwest; +//use crate::data_toml; +use crate::url; + +pub async fn get_request(user: String) -> String { + //let token = data_refresh(&"access"); + let url = url(&"describe"); + + let client = reqwest::Client::new(); + let res = client + .get(url) + .query(&[("repo", &user)]) + //.header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/follow.rs b/src/follow.rs new file mode 100644 index 0000000..d1317f6 --- /dev/null +++ b/src/follow.rs @@ -0,0 +1,81 @@ +extern crate reqwest; +use crate::data_toml; +use crate::data_refresh; +use crate::url; +use iso8601_timestamp::Timestamp; +use serde_json::json; + +//use crate::data::Follow; + +pub async fn post_request(u: String) -> String { + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_create"); + let col = "app.bsky.graph.follow".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "repo": handle.to_string(), + "did": did.to_string(), + "collection": col.to_string(), + "record": { + "subject": u.to_string(), + "createdAt": d.to_string(), + }, + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} + +pub async fn delete_request(u: String, rkey: String) -> String { + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_delete"); + let col = "app.bsky.graph.follow".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "repo": handle.to_string(), + "did": did.to_string(), + "collection": col.to_string(), + "rkey": rkey.to_string(), + "record": { + "subject": u.to_string(), + "createdAt": d.to_string(), + }, + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/followers.rs b/src/followers.rs new file mode 100644 index 0000000..0927289 --- /dev/null +++ b/src/followers.rs @@ -0,0 +1,24 @@ +extern crate reqwest; +use crate::data_refresh; +use crate::url; +//use serde_json::json; + +pub async fn get_request(actor: String, cursor: Option) -> String { + let token = data_refresh(&"access"); + let url = url(&"followers"); + let cursor = cursor.unwrap(); + + let client = reqwest::Client::new(); + let res = client + .get(url) + .query(&[("actor", actor), ("cursor", cursor)]) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/follows.rs b/src/follows.rs new file mode 100644 index 0000000..bb517e0 --- /dev/null +++ b/src/follows.rs @@ -0,0 +1,26 @@ +extern crate reqwest; +use crate::data_refresh; +use crate::url; +//use serde_json::json; + +pub async fn get_request(actor: String, cursor: Option) -> String { + let token = data_refresh(&"access"); + let url = url(&"follows"); + let cursor = cursor.unwrap(); + //let cursor = "1682386039125::bafyreihwgwozmvqxcxrhbr65agcaa4v357p27ccrhzkjf3mz5xiozjvzfa".to_string(); + //let cursor = "1682385956974::bafyreihivhux5m3sxbg33yruhw5ozhahwspnuqdsysbo57smzgptdcluem".to_string(); + + let client = reqwest::Client::new(); + let res = client + .get(url) + .query(&[("actor", actor), ("cursor", cursor)]) + //cursor.unwrap() + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + return res; +} diff --git a/src/img.rs b/src/img.rs new file mode 100644 index 0000000..013917b --- /dev/null +++ b/src/img.rs @@ -0,0 +1,59 @@ +extern crate reqwest; +use crate::data_toml; +use crate::data_refresh; +use crate::url; +use serde_json::json; +use iso8601_timestamp::Timestamp; + +pub async fn post_request(text: String, link: String) -> String { + + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_create"); + let col = "app.bsky.feed.post".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "repo": handle.to_string(), + "did": did.to_string(), + "collection": col.to_string(), + "record": { + "createdAt": d.to_string(), + "text": text.to_string(), + "embed": { + "$type": "app.bsky.embed.images", + "images": [ + { + "alt": "", + "image": { + "$type":"blob", + "ref": { + "$link" : link.to_string() + }, + "mimeType": "image/png", + "size": 0 + } + } + ] + } + } + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res +} diff --git a/src/img_reply.rs b/src/img_reply.rs new file mode 100644 index 0000000..f7b3a9a --- /dev/null +++ b/src/img_reply.rs @@ -0,0 +1,74 @@ +extern crate reqwest; +use crate::data_toml; +use crate::data_refresh; +use crate::url; +use iso8601_timestamp::Timestamp; +use serde_json::json; + +pub async fn post_request( + text: String, + link: String, + cid: String, + uri: String, + itype: String, +) -> String { + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_create"); + let col = "app.bsky.feed.post".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "repo": handle.to_string(), + "did": did.to_string(), + "collection": col.to_string(), + "record": { + "createdAt": d.to_string(), + "text": text.to_string(), + "embed": { + "$type": "app.bsky.embed.images", + "images": [ + { + "alt": "", + "image": { + "$type":"blob", + "ref": { + "$link" : link.to_string() + }, + "mimeType": itype.to_string(), + "size": 0 + } + } + ] + }, + "reply": { + "root": { + "cid": cid.to_string(), + "uri": uri.to_string() + }, + "parent": { + "cid": cid.to_string(), + "uri": uri.to_string() + } + } + } + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/img_upload.rs b/src/img_upload.rs new file mode 100644 index 0000000..013917b --- /dev/null +++ b/src/img_upload.rs @@ -0,0 +1,59 @@ +extern crate reqwest; +use crate::data_toml; +use crate::data_refresh; +use crate::url; +use serde_json::json; +use iso8601_timestamp::Timestamp; + +pub async fn post_request(text: String, link: String) -> String { + + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_create"); + let col = "app.bsky.feed.post".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "repo": handle.to_string(), + "did": did.to_string(), + "collection": col.to_string(), + "record": { + "createdAt": d.to_string(), + "text": text.to_string(), + "embed": { + "$type": "app.bsky.embed.images", + "images": [ + { + "alt": "", + "image": { + "$type":"blob", + "ref": { + "$link" : link.to_string() + }, + "mimeType": "image/png", + "size": 0 + } + } + ] + } + } + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res +} diff --git a/src/like.rs b/src/like.rs new file mode 100644 index 0000000..ffc4821 --- /dev/null +++ b/src/like.rs @@ -0,0 +1,45 @@ +extern crate reqwest; +use crate::data_toml; +use crate::data_refresh; +use crate::url; +use iso8601_timestamp::Timestamp; +use serde_json::json; + +pub async fn post_request(cid: String, uri: String) -> String { + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_create"); + let col = "app.bsky.feed.like".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "repo": handle.to_string(), + "did": did.to_string(), + "collection": col.to_string(), + "record": { + "subject": { + "uri": uri.to_string(), + "cid": cid.to_string() + }, + "createdAt": d.to_string(), + }, + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e86ebf3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,599 @@ +use seahorse::{App, Command, Context, Flag, FlagType}; +use std::env; + +use crate::ascii::c_ascii; +use crate::bot::c_bot; +use crate::data::c_follow_all; +use crate::data::c_openai_key; +use crate::data::data_toml; +use crate::data::data_refresh; +use crate::data::url; +use crate::data::w_cfg; +use crate::data::w_refresh; + +use data::ProfileIdentityResolve; + +pub mod ascii; +pub mod bot; +pub mod data; +pub mod describe; +pub mod follow; +pub mod followers; +pub mod follows; +pub mod img_reply; +pub mod like; +pub mod mention; +pub mod notify; +pub mod notify_read; +pub mod openai; +pub mod post; +pub mod post_link; +pub mod profile; +pub mod refresh; +pub mod reply; +pub mod reply_link; +pub mod reply_og; +pub mod repost; +pub mod session; +pub mod timeline_author; +pub mod token; + +fn main() { + let args: Vec = env::args().collect(); + let app = App::new(env!("CARGO_PKG_NAME")) + .command( + Command::new("ai") + .alias("a") + .action(ascii_art) + .flag( + Flag::new("type", FlagType::String) + .description("type flag") + .alias("t"), + ) + ) + .command( + Command::new("bot") + .alias("b") + .action(bot) + .flag( + Flag::new("admin", FlagType::String) + .alias("a"), + ) + ) + .command( + Command::new("follow_all") + .action(follow_all), + ) + .command( + Command::new("login") + .alias("l") + .description("l -p \n\t\t\tl -p -s ") + .action(token) + .flag( + Flag::new("password", FlagType::String) + .description("password flag") + .alias("p"), + ) + .flag( + Flag::new("server", FlagType::String) + .description("server flag") + .alias("s"), + ) + ) + .command( + Command::new("refresh") + .alias("r") + .action(refresh), + ) + .command( + Command::new("notify") + .alias("n") + .action(notify), + ) + .command( + Command::new("timeline") + .alias("t") + .action(timeline), + ) + .command( + Command::new("did") + .description("did ") + .action(did) + ) + .command( + Command::new("post") + .description("p ") + .alias("p") + .action(post) + .flag( + Flag::new("link", FlagType::String) + .alias("l"), + ) + ) + .command( + Command::new("like") + .description("like -u ") + .action(like) + .flag( + Flag::new("uri", FlagType::String) + .alias("u"), + ) + ) + .command( + Command::new("repost") + .description("repost -u ") + .action(repost) + .flag( + Flag::new("uri", FlagType::String) + .alias("u"), + ) + ) + .command( + Command::new("reply-og") + .description("reply-og -c -u -i -t -d <description> -l <link>") + .action(reply_og) + .flag( + Flag::new("uri", FlagType::String) + .alias("u"), + ) + .flag( + Flag::new("cid", FlagType::String) + .alias("c"), + ) + .flag( + Flag::new("uri-root", FlagType::String) + ) + .flag( + Flag::new("cid-root", FlagType::String) + ) + .flag( + Flag::new("link", FlagType::String) + .alias("l"), + ) + .flag( + Flag::new("title", FlagType::String) + .alias("t"), + ) + .flag( + Flag::new("description", FlagType::String) + .alias("d"), + ) + .flag( + Flag::new("img", FlagType::String) + .alias("i"), + ) + ) + .command( + Command::new("reply") + .description("r <text> -u <uri> -c <cid>") + .action(reply) + .flag( + Flag::new("uri", FlagType::String) + .alias("u"), + ) + .flag( + Flag::new("cid", FlagType::String) + .alias("c"), + ) + .flag( + Flag::new("link", FlagType::String) + .description("-l <link>") + .alias("l"), + ) + ) + .command( + Command::new("mention") + .description("@ <handle> -p <text>") + .alias("@") + .action(mention) + .flag( + Flag::new("post", FlagType::String) + .alias("p"), + ) + ) + .command( + Command::new("follow") + .description("follow <did>") + .action(follow) + .flag( + Flag::new("follows", FlagType::Bool) + .alias("s"), + ) + .flag( + Flag::new("delete", FlagType::String) + .alias("d"), + ) + .flag( + Flag::new("followers", FlagType::Bool) + .alias("w"), + ) + .flag( + Flag::new("all", FlagType::Bool) + .alias("a"), + ) + .flag( + Flag::new("cursor", FlagType::String) + .alias("c"), + ) + ) + .command( + Command::new("profile") + .description("pro <handle>") + .alias("pro") + .action(profile) + ) + .command( + Command::new("img-upload") + .description("img-upload <img>") + .action(img_upload) + ) + .command( + Command::new("img-post") + .description("img-post <text> -l <link> -u <uri> -c <cid>") + .action(img_post) + .flag( + Flag::new("link", FlagType::String) + .alias("l"), + ) + .flag( + Flag::new("uri", FlagType::String) + .alias("u"), + ) + .flag( + Flag::new("cid", FlagType::String) + .alias("c"), + ) + ) + .command( + Command::new("openai") + .description("openai <text>") + .alias("chat") + .action(openai) + ) + .command( + Command::new("openai-key") + .description("openai-key <API_KEY>") + .action(openai_key), + ) + ; + app.run(args); +} + +fn ascii_art(c: &Context) { + if let Ok(t) = c.string_flag("type") { + c_ascii(&t); + } else { + c_ascii("color"); + } +} + +fn bot(c: &Context) { + refresh(c); + loop { + c_bot(c); + } +} + +fn follow_all(_c: &Context) { + c_follow_all(); +} + +fn openai_key(c: &Context) { + c_openai_key(c); +} + +fn token(c: &Context) { + let m = c.args[0].to_string(); + let h = async { + if let Ok(p) = c.string_flag("password") { + if let Ok(s) = c.string_flag("server") { + let res = token::post_request(m.to_string(), p.to_string(), s.to_string()).await; + w_cfg(&s, &res, &p); + } else { + let res = + token::post_request(m.to_string(), p.to_string(), "bsky.social".to_string()) + .await; + w_cfg(&"bsky.social", &res, &p); + } + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn refresh(_c: &Context) { + let h = async { + let session = session::get_request().await; + if session == "err" { + let res = refresh::post_request().await; + println!("{}", res); + if res == "err" { + let m = data_toml(&"handle"); + let p = data_toml(&"password"); + let s = data_toml(&"host"); + println!("handle:{}, pass:{}, host:{}", m, p, s); + let res = token::post_request(m.to_string(), p.to_string(), s.to_string()).await; + w_cfg(&s, &res, &p); + println!("res:{}", res); + } else { + w_refresh(&res); + } + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn notify(c: &Context) { + refresh(c); + let h = async { + let j = notify::get_request(100).await; + println!("{}", j); + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn did(c: &Context) { + refresh(c); + let h = async { + if c.args.len() == 0 { + let j = describe::get_request(data_toml(&"handle")).await; + println!("{}", j); + } else { + let j = describe::get_request(c.args[0].to_string()).await; + println!("{}", j); + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn timeline(c: &Context) { + refresh(c); + let h = async { + if c.args.len() == 0 { + let str = timeline_author::get_request(data_toml(&"handle").to_string()); + println!("{}", str.await); + } else { + let str = timeline_author::get_request(c.args[0].to_string()); + println!("{}", str.await); + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn post(c: &Context) { + refresh(c); + let m = c.args[0].to_string(); + let h = async { + if let Ok(link) = c.string_flag("link") { + let e = link.chars().count(); + let s = 0; + let str = + post_link::post_request(m.to_string(), link.to_string(), s, e.try_into().unwrap()); + println!("{}", str.await); + } else { + let str = post::post_request(m.to_string()); + println!("{}", str.await); + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn like(c: &Context) { + refresh(c); + let m = c.args[0].to_string(); + let h = async { + if let Ok(uri) = c.string_flag("uri") { + let str = like::post_request(m.to_string(), uri); + println!("{}", str.await); + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn repost(c: &Context) { + refresh(c); + let m = c.args[0].to_string(); + let h = async { + if let Ok(uri) = c.string_flag("uri") { + let str = repost::post_request(m.to_string(), uri); + println!("{}", str.await); + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn follow(c: &Context) { + refresh(c); + let m = c.args[0].to_string(); + let h = async { + let handle = data_toml(&"handle"); + if let Ok(cursor) = c.string_flag("cursor") { + let str = follow::post_request(m.to_string()); + println!("{}", str.await); + //let str = follow::delete_request(m.to_string(), delete.to_string()); + //let str = followers::get_request(handle,Some("".to_string())); + //let str = followers::get_request(handle,Some(cursor.to_string())); + //let str = follows::get_request(handle,Some("".to_string())); + let str = follows::get_request(handle, Some(cursor.to_string())); + println!("{}", str.await); + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn profile(c: &Context) { + refresh(c); + let h = async { + if c.args.len() == 0 { + let j = profile::get_request(data_toml(&"handle")).await; + println!("{}", j); + } else { + let j = profile::get_request(c.args[0].to_string()).await; + println!("{}", j); + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn mention(c: &Context) { + refresh(c); + let m = c.args[0].to_string(); + let h = async { + let str = profile::get_request(m.to_string()).await; + let profile: ProfileIdentityResolve = serde_json::from_str(&str).unwrap(); + let udid = profile.did; + let handle = m.to_string(); + let at = "@".to_owned() + &handle; + let e = at.chars().count(); + let s = 0; + if let Ok(post) = c.string_flag("post") { + let str = mention::post_request( + post.to_string(), + at.to_string(), + udid.to_string(), + s, + e.try_into().unwrap(), + ) + .await; + println!("{}", str); + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn reply(c: &Context) { + refresh(c); + let m = c.args[0].to_string(); + let h = async { + if let Ok(cid) = c.string_flag("cid") { + if let Ok(uri) = c.string_flag("uri") { + if let Ok(link) = c.string_flag("link") { + let s = 0; + let e = link.chars().count(); + let str = reply_link::post_request( + m.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid.to_string(), + uri.to_string(), + ) + .await; + println!("{}", str); + } else { + let str = reply::post_request( + m.to_string(), + cid.to_string(), + uri.to_string(), + cid.to_string(), + uri.to_string(), + ) + .await; + println!("{}", str); + } + } + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +#[tokio::main] +async fn c_img_upload(c: &Context) -> reqwest::Result<()> { + let token = data_refresh(&"access"); + let atoken = "Authorization: Bearer ".to_owned() + &token; + let con = "Content-Type: image/png"; + let url = url(&"upload_blob"); + let f = "@".to_owned() + &c.args[0].to_string(); + use std::process::Command; + let output = Command::new("curl") + .arg("-X") + .arg("POST") + .arg("-sL") + .arg("-H") + .arg(&con) + .arg("-H") + .arg(&atoken) + .arg("--data-binary") + .arg(&f) + .arg(&url) + .output() + .expect("curl"); + let d = String::from_utf8_lossy(&output.stdout); + let d = d.to_string(); + println!("{}", d); + Ok(()) +} + +fn img_upload(c: &Context) { + refresh(c); + c_img_upload(c).unwrap(); +} + +fn img_post(c: &Context) { + let m = c.args[0].to_string(); + let link = c.string_flag("link").unwrap(); + let cid = c.string_flag("cid").unwrap(); + let uri = c.string_flag("uri").unwrap(); + let h = async { + let itype = "image/png"; + let str = img_reply::post_request( + m.to_string(), + link.to_string(), + cid.to_string(), + uri.to_string(), + itype.to_string(), + ) + .await; + println!("{}", str); + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn reply_og(c: &Context) { + refresh(c); + + //let mut admin = "".to_string(); + //if c.string_flag("admin").is_ok() { + // admin = c.string_flag("admin").unwrap(); + //} + + let m = c.args[0].to_string(); + let link = c.string_flag("link").unwrap(); + let cid = c.string_flag("cid").unwrap(); + let uri = c.string_flag("uri").unwrap(); + let cid_root = c.string_flag("cid-root").unwrap(); + let uri_root = c.string_flag("uri-root").unwrap(); + let title = c.string_flag("title").unwrap(); + let desc = c.string_flag("description").unwrap(); + let img = c.string_flag("img").unwrap(); + let h = async { + let str = reply_og::post_request(m, link, cid, uri, cid_root, uri_root, img, title, desc); + println!("{}", str.await); + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + +fn openai(c: &Context) { + let m = c.args[0].to_string(); + let h = async { + let str = openai::post_request(m.to_string()).await; + println!("{}", str); + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} diff --git a/src/mention.rs b/src/mention.rs new file mode 100644 index 0000000..84cf978 --- /dev/null +++ b/src/mention.rs @@ -0,0 +1,57 @@ +extern crate reqwest; +use crate::data_toml; +use crate::data_refresh; +use crate::url; +use iso8601_timestamp::Timestamp; +use serde_json::json; + +pub async fn post_request(text: String, at: String, udid: String, s: i32, e: i32) -> String { + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_create"); + let col = "app.bsky.feed.post".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "did": did.to_string(), + "repo": handle.to_string(), + "collection": col.to_string(), + "record": { + "text": at.to_string() + &" ".to_string() + &text.to_string(), + "$type": "app.bsky.feed.post", + "createdAt": d.to_string(), + "facets": [ + { + "$type": "app.bsky.richtext.facet", + "index": { + "byteEnd": e, + "byteStart": s + },"features": [ + { + "did": udid.to_string(), + "$type": "app.bsky.richtext.facet#mention" + } + ] + } + ] + }, + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/notify.rs b/src/notify.rs new file mode 100644 index 0000000..cd74660 --- /dev/null +++ b/src/notify.rs @@ -0,0 +1,30 @@ +extern crate reqwest; +use crate::data_refresh; +use crate::url; +//use serde_json::json; + +pub async fn get_request(limit: i32) -> String { + let token = data_refresh(&"access"); + let url = url(&"notify_list"); + + let client = reqwest::Client::new(); + let res = client + .get(url) + .query(&[("limit", limit)]) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap(); + + let status_ref = res.error_for_status_ref(); + + match status_ref { + Ok(_) => { + return res.text().await.unwrap(); + } + Err(_e) => { + let e = "err".to_string(); + return e; + } + } +} diff --git a/src/notify_read.rs b/src/notify_read.rs new file mode 100644 index 0000000..e8941ec --- /dev/null +++ b/src/notify_read.rs @@ -0,0 +1,27 @@ +extern crate reqwest; +use crate::data_refresh; +use crate::url; +use serde_json::json; + +pub async fn post_request(time: String) -> String { + let token = data_refresh(&"access"); + let url = url(&"notify_update"); + + let post = Some(json!({ + "seenAt": time.to_string(), + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/openai.rs b/src/openai.rs new file mode 100644 index 0000000..e7e2b07 --- /dev/null +++ b/src/openai.rs @@ -0,0 +1,58 @@ +extern crate reqwest; +use crate::data::Open; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type")] +struct OpenChar { + choices: Vec<ChoicesChar>, +} + +#[derive(Serialize, Deserialize, Debug)] +struct ChoicesChar { + message: OpenContent, +} + +#[derive(Serialize, Deserialize, Debug)] +struct OpenContent { + content: String, +} + +pub async fn post_request(prompt: String) -> String { + let data = Open::new().unwrap(); + let data = Open { api: data.api }; + + let setting = "あなたはyuiという作品のアイと呼ばれるキャラクターです。第一人称は「アイ」です。 + 第二人称はできる限り省略してください。 + 自由気ままな優しさを兼ね備えます。 + 口調例:アイね、ここでお昼寝するの。 + 幼い子供の口調で話してください。 + 誕生日は1月23日。 + 自分を信じて。 + "; + + let post = Some(json!({ + "model": "gpt-3.5-turbo", + "messages": [ + {"role": "system", "content": &setting.to_string()}, + {"role": "user", "content": &prompt.to_string()}, + ] + })); + + let client = reqwest::Client::new(); + let res = client + .post("https://api.openai.com/v1/chat/completions") + .header("Authorization", "Bearer ".to_owned() + &data.api) + .json(&post) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + let p: OpenChar = serde_json::from_str(&res).unwrap(); + let o = &p.choices[0].message.content; + + return o.to_string(); +} diff --git a/src/post.rs b/src/post.rs new file mode 100644 index 0000000..c90992b --- /dev/null +++ b/src/post.rs @@ -0,0 +1,42 @@ +extern crate reqwest; +use crate::data_toml; +use crate::data_refresh; +use crate::url; +use iso8601_timestamp::Timestamp; +use serde_json::json; + +pub async fn post_request(text: String) -> String { + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_create"); + let col = "app.bsky.feed.post".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "repo": handle.to_string(), + "did": did.to_string(), + "collection": col.to_string(), + "record": { + "text": text.to_string(), + "createdAt": d.to_string(), + }, + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/post_link.rs b/src/post_link.rs new file mode 100644 index 0000000..ba50aea --- /dev/null +++ b/src/post_link.rs @@ -0,0 +1,56 @@ +extern crate reqwest; +use crate::data_toml; +use crate::data_refresh; +use crate::url; +use iso8601_timestamp::Timestamp; +use serde_json::json; + +pub async fn post_request(text: String, link: String, s: i32, e: i32) -> String { + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_create"); + let col = "app.bsky.feed.post".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "repo": handle.to_string(), + "did": did.to_string(), + "collection": col.to_string(), + "record": { + "text": link.to_string() + &" ".to_string() + &text.to_string(), + "createdAt": d.to_string(), + "facets": [ + { + "index": { + "byteStart": s, + "byteEnd": e + }, + "features": [ + { + "$type": "app.bsky.richtext.facet#link", + "uri": link.to_string() + } + ] + } + ], + }, + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/profile.rs b/src/profile.rs new file mode 100644 index 0000000..6c49ebf --- /dev/null +++ b/src/profile.rs @@ -0,0 +1,21 @@ +extern crate reqwest; +use crate::data_refresh; +use crate::url; + +pub async fn get_request(user: String) -> String { + let token = data_refresh(&"access"); + let url = url(&"profile_get") + &"?handle=" + &user; + + let client = reqwest::Client::new(); + let res = client + .get(url) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/refresh.rs b/src/refresh.rs new file mode 100644 index 0000000..06fe322 --- /dev/null +++ b/src/refresh.rs @@ -0,0 +1,28 @@ +extern crate reqwest; +use crate::data_toml; +use crate::url; + +pub async fn post_request() -> String { + let refresh = data_toml(&"refresh"); + let url = url(&"session_refresh"); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .header("Authorization", "Bearer ".to_owned() + &refresh) + .send() + .await + .unwrap(); + + let status_ref = res.error_for_status_ref(); + + match status_ref { + Ok(_) => { + return res.text().await.unwrap(); + } + Err(_e) => { + let e = "err".to_string(); + return e; + } + } +} diff --git a/src/reply.rs b/src/reply.rs new file mode 100644 index 0000000..812229f --- /dev/null +++ b/src/reply.rs @@ -0,0 +1,59 @@ +extern crate reqwest; +use crate::data_toml; +use crate::data_refresh; +use crate::url; +use iso8601_timestamp::Timestamp; +use serde_json::json; + +pub async fn post_request( + text: String, + cid: String, + uri: String, + cid_root: String, + uri_root: String, +) -> String { + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_create"); + //let url = "https://bsky.social/xrpc/com.atproto.repo.createRecord"; + let col = "app.bsky.feed.post".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "repo": handle.to_string(), + "did": did.to_string(), + "collection": col.to_string(), + "record": { + "text": text.to_string(), + "createdAt": d.to_string(), + "reply": { + "root": { + "cid": cid_root.to_string(), + "uri": uri_root.to_string() + }, + "parent": { + "cid": cid.to_string(), + "uri": uri.to_string() + } + } + }, + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/reply_link.rs b/src/reply_link.rs new file mode 100644 index 0000000..d34b9eb --- /dev/null +++ b/src/reply_link.rs @@ -0,0 +1,75 @@ +extern crate reqwest; +use crate::data_toml; +use crate::data_refresh; +use crate::url; +use iso8601_timestamp::Timestamp; +use serde_json::json; + +pub async fn post_request( + text: String, + link: String, + s: i32, + e: i32, + cid: String, + uri: String, + cid_root: String, + uri_root: String, +) -> String { + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_create"); + let col = "app.bsky.feed.post".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "repo": handle.to_string(), + "did": did.to_string(), + "collection": col.to_string(), + "record": { + "text": link.to_string() + &" ".to_string() + &text.to_string(), + "createdAt": d.to_string(), + "reply": { + "root": { + "cid": cid_root.to_string(), + "uri": uri_root.to_string() + }, + "parent": { + "cid": cid.to_string(), + "uri": uri.to_string() + } + }, + "facets": [ + { + "index": { + "byteStart": s, + "byteEnd": e + }, + "features": [ + { + "$type": "app.bsky.richtext.facet#link", + "uri": link.to_string() + } + ] + } + ], + }, + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/reply_og.rs b/src/reply_og.rs new file mode 100644 index 0000000..8687a2d --- /dev/null +++ b/src/reply_og.rs @@ -0,0 +1,78 @@ +extern crate reqwest; +use crate::data_toml; +use crate::data_refresh; +use crate::url; +use iso8601_timestamp::Timestamp; +use serde_json::json; + +pub async fn post_request( + m: String, + link: String, + cid: String, + uri: String, + cid_root: String, + uri_root: String, + img: String, + title: String, + description: String, +) -> String { + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_create"); + let col = "app.bsky.feed.post".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "repo": handle.to_string(), + "did": did.to_string(), + "collection": col.to_string(), + "record": { + "createdAt": d.to_string(), + "text": m.to_string(), + "embed": { + "$type": "app.bsky.embed.external", + "external": { + "uri": link.to_string(), + "thumb": { + "$type": "blob", + "ref": { + "$link": img.to_string() + }, + "mimeType": "image/jpeg", + "size": 0 + }, + "title": title.to_string(), + "description": description.to_string() + } + }, + "reply": { + "root": { + "cid": cid_root.to_string(), + "uri": uri_root.to_string() + }, + "parent": { + "cid": cid.to_string(), + "uri": uri.to_string() + } + } + } + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/repost.rs b/src/repost.rs new file mode 100644 index 0000000..44f202a --- /dev/null +++ b/src/repost.rs @@ -0,0 +1,45 @@ +extern crate reqwest; +use crate::data_toml; +use crate::data_refresh; +use crate::url; +use iso8601_timestamp::Timestamp; +use serde_json::json; + +pub async fn post_request(cid: String, uri: String) -> String { + let token = data_refresh(&"access"); + let did = data_toml(&"did"); + let handle = data_toml(&"handle"); + + let url = url(&"record_create"); + let col = "app.bsky.feed.repost".to_string(); + + let d = Timestamp::now_utc(); + let d = d.to_string(); + + let post = Some(json!({ + "repo": handle.to_string(), + "did": did.to_string(), + "collection": col.to_string(), + "record": { + "subject": { + "uri": uri.to_string(), + "cid": cid.to_string() + }, + "createdAt": d.to_string(), + }, + })); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&post) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..ad49cb1 --- /dev/null +++ b/src/session.rs @@ -0,0 +1,28 @@ +extern crate reqwest; +use crate::data_refresh; +use crate::url; + +pub async fn get_request() -> String { + let token = data_refresh(&"access"); + let url = url(&"session_get"); + + let client = reqwest::Client::new(); + let res = client + .get(url) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap(); + + let status_ref = res.error_for_status_ref(); + + match status_ref { + Ok(_) => { + return res.text().await.unwrap(); + } + Err(_e) => { + let e = "err".to_string(); + return e; + } + } +} diff --git a/src/timeline_author.rs b/src/timeline_author.rs new file mode 100644 index 0000000..bbb447e --- /dev/null +++ b/src/timeline_author.rs @@ -0,0 +1,27 @@ +extern crate reqwest; +use crate::data_refresh; +use crate::url; + +pub async fn get_request(actor: String) -> String { + let token = data_refresh(&"access"); + let url = url(&"record_list"); + + let actor = actor.to_string(); + //let cursor = cursor.unwrap(); + + let col = "app.bsky.feed.post".to_string(); + let client = reqwest::Client::new(); + let res = client + .get(url) + .query(&[("repo", actor), ("collection", col)]) + //.query(&[("actor", actor),("cursor", cursor)]) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/src/token.rs b/src/token.rs new file mode 100644 index 0000000..29ed701 --- /dev/null +++ b/src/token.rs @@ -0,0 +1,25 @@ +extern crate reqwest; +use std::collections::HashMap; + +pub async fn post_request(handle: String, pass: String, host: String) -> String { + let url = "https://".to_owned() + + &host.to_string() + + &"/xrpc/com.atproto.server.createSession".to_string(); + + let mut map = HashMap::new(); + map.insert("identifier", &handle); + map.insert("password", &pass); + + let client = reqwest::Client::new(); + let res = client + .post(url) + .json(&map) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + return res; +} diff --git a/test/ai.zsh b/test/ai.zsh new file mode 100755 index 0000000..ce07dc0 --- /dev/null +++ b/test/ai.zsh @@ -0,0 +1,39 @@ +#!/bin/zsh + +# https://www.docs.bsky.app/docs/get-started + +case $OSTYPE in + darwin*) + alias date="/opt/homebrew/bin/gdate" + ;; +esac +d=${0:a:h} + +source $d/env +source $d/refresh.zsh +source $d/token.zsh +source $d/reply.zsh +source $d/notify.zsh +source $d/notify_cid.zsh +source $d/cron.zsh + +case $1 in + refresh|r) + refresh + ;; + token|t) + token + ;; + reply|rep) + reply + ;; + notify|n) + notify + ;; + cron|c) + cron + ;; + cid) + cid + ;; +esac diff --git a/test/cron.zsh b/test/cron.zsh new file mode 100644 index 0000000..e9f389a --- /dev/null +++ b/test/cron.zsh @@ -0,0 +1,7 @@ +function cron() { + t=`docker ps |grep aios|grep Up` + if [ -z "$t" ];then + docker compose up -d + fi + exit +} diff --git a/test/entrypoint.sh b/test/entrypoint.sh new file mode 100644 index 0000000..bed078b --- /dev/null +++ b/test/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/zsh + +ai l $HANDLE -p $PASSWORD -s $HOST && ai bot -a $ADMIN diff --git a/test/env b/test/env new file mode 100644 index 0000000..863b1c5 --- /dev/null +++ b/test/env @@ -0,0 +1,18 @@ +cfg=~/.config/ai/test.json + +if [ -f $cfg ];then + host=`cat $cfg|jq -r .host` + handle=`cat $cfg|jq -r .handle` + pass=`cat $cfg|jq -r .password` + date=`date --iso-8601=seconds` +fi + +if [ -f $cfg.t ];then + token=`cat $cfg.t|jq -r .accessJwt` + refresh=`cat $cfg.t|jq -r .refreshJwt` + did=`cat $cfg.t|jq -r .did` +fi + +if [ ! -d $d/json ];then + mkdir -p $d/json +fi diff --git a/test/notify.zsh b/test/notify.zsh new file mode 100644 index 0000000..8f5879f --- /dev/null +++ b/test/notify.zsh @@ -0,0 +1,37 @@ +function notify() { + url=https://$host/xrpc/app.bsky.notification.listNotifications + f=$d/json/notify.json + if [ ! -f $f ];then + curl -sL "Content-Type: application/json" -H "Authorization: Bearer $token" "$url?limit=100" >! $f + fi + + for ((i=0;i<=99;i++)) + do + echo "[$i]---" + cid=`cat $f|jq -r ".|.[].[$i]?|.cid?"` + uri=`cat $f|jq -r ".|.[].[$i]?|.uri?"` + echo cid: $cid + echo uri: $uri + cid_r=`cat $f|jq -r ".[]|.[$i]?|.record.reply.root.cid?"` + + if [ "$cid_r" = "null" ];then + continue + fi + uri_r=`cat $f|jq -r ".[]|.[$i]?|.record.reply.root.uri?"` + cid_p=`cat $f|jq -r ".[]|.[$i]?|.record.reply.parent.cid?"` + uri_p=`cat $f|jq -r ".[]|.[$i]?|.record.reply.parent.uri?"` + did_p=`echo $uri_p|cut -d / -f 3` + if [ "$did_p" != "did:plc:uqzpqmrjnptsxezjx4xuh2mn" ];then + continue + fi + echo cid_root: $cid_r + echo uri_root: $uri_r + echo cid_parent: $cid_p + echo uri_parent: $uri_p + echo --- + echo uri: $uri|sed "s#at://#https://bsky.app/profile/#g"|sed 's/app.bsky.feed.post/post/g' + echo uri_root: $uri_r|sed "s#at://#https://bsky.app/profile/#g"|sed 's/app.bsky.feed.post/post/g' + echo uri_parent: $uri_p|sed "s#at://#https://bsky.app/profile/#g"|sed 's/app.bsky.feed.post/post/g' + echo --- + done +} diff --git a/test/notify_cid.zsh b/test/notify_cid.zsh new file mode 100644 index 0000000..412275f --- /dev/null +++ b/test/notify_cid.zsh @@ -0,0 +1,16 @@ + +function cid(){ + dd=${d:h} + ai=$dd/target/debug/ai + txt=$dd/.config/ai/txt + f=$txt/notify_cid + if [ ! -f $ai ];then + cd $dd + cargo build + fi + if [ ! -d $txt ];then + mkdir -p $txt + fi + $ai n|jq -r ".[]|.[]?.cid" >> $f.txt + cp -rf $f.txt ${f}_run.txt +} diff --git a/test/refresh.zsh b/test/refresh.zsh new file mode 100755 index 0000000..81998ce --- /dev/null +++ b/test/refresh.zsh @@ -0,0 +1,11 @@ +function refresh(){ + token=`cat $cfg.t|jq -r .accessJwt` + refresh=`cat $cfg.t|jq -r .refreshJwt` + if [ ! -f $cfg ];then + token + fi + url=https://$host/xrpc/com.atproto.server.refreshSession + j=`curl -sL -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $refresh" $url` + echo $j + echo $j >! $cfg.t +} diff --git a/test/reply.zsh b/test/reply.zsh new file mode 100755 index 0000000..c0ebed4 --- /dev/null +++ b/test/reply.zsh @@ -0,0 +1,40 @@ +function reply() { + + #uri: https://bsky.app/profile/did:plc:4hqjfn7m6n5hno3doamuhgef/post/3kkumyv72w22o + #uri_root: https://bsky.app/profile/did:plc:uqzpqmrjnptsxezjx4xuh2mn/post/3kkumysfipk2p + #uri_parent: https://bsky.app/profile/did:plc:uqzpqmrjnptsxezjx4xuh2mn/post/3kkumysfipk2p + + cid=bafyreiaxz6hbqgylsxglqita73c5gzxzoatupgitd35rwjpd6dzpa4ctwi + uri=at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.post/3kkumyv72w22o + cid_root=bafyreiacxuk4ypaxbg7qxnmrvpnaej5o7azewqioelfgbuikp77jevy6hq + uri_root=at://did:plc:uqzpqmrjnptsxezjx4xuh2mn/app.bsky.feed.post/3kkumysfipk2p + cid_parent=bafyreiacxuk4ypaxbg7qxnmrvpnaej5o7azewqioelfgbuikp77jevy6hq + uri_parent=at://did:plc:uqzpqmrjnptsxezjx4xuh2mn/app.bsky.feed.post/3kkumysfipk2p + + url="https://$host/xrpc/com.atproto.repo.createRecord" + col="app.bsky.feed.post" + +json="{ + \"repo\": \"$handle\", + \"did\": \"$did\", + \"collection\": \"$col\", + \"record\": { + \"text\": \"$text\", + \"createdAt\": \"$date\", + \"reply\": { + \"root\": { + \"cid\": \"$cid_root\", + \"uri\": \"$uri_root\" + }, + \"parent\": { + \"cid\": \"$cid\", + \"uri\": \"$uri\" + } + } + } +}" + + echo $json|jq . + url=https://$host/xrpc/com.atproto.repo.createRecord + curl -sL -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d "$json" $url +} diff --git a/test/token.zsh b/test/token.zsh new file mode 100755 index 0000000..c9a5d24 --- /dev/null +++ b/test/token.zsh @@ -0,0 +1,18 @@ +function token() { + mkdir -p ~/.config/ai + echo server: + read host + + echo password: + read pass + + echo handle: + read handle + + echo "{ \"host\":\"$host\", \"password\":\"$pass\", \"handle\":\"$handle\" }" >> $cfg + + url=https://$host/xrpc/com.atproto.server.createSession + j=`curl -sL -X POST -H "Content-Type: application/json" -d "{\"identifier\":\"$handle\",\"password\":\"$pass\"}" $url` + echo $j + echo $j >! $cfg.t +}