Compare commits
No commits in common. "main" and "0.1.1" have entirely different histories.
|
@ -1,71 +0,0 @@
|
|||
name: Build development version
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- .github/**
|
||||
- src/**
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- .github/**
|
||||
- src/**
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
- name: Upload Linux Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: hey-linux
|
||||
path: target/release/hey
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
- name: Upload macOS Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: hey-macos
|
||||
path: target/release/hey
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
- name: Upload Windows Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: hey-windows
|
||||
path: target/release/hey.exe
|
File diff suppressed because it is too large
Load Diff
|
@ -11,7 +11,6 @@ clap = { version = "4.5.4", features = ["derive"] }
|
|||
femme = "2.2.1"
|
||||
home = "0.5.9"
|
||||
log = "0.4.21"
|
||||
openssl = { version = "0.10.68", features = ["vendored"] }
|
||||
reqwest = "0.12.3"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0.115"
|
||||
|
|
35
README.md
35
README.md
|
@ -2,16 +2,29 @@
|
|||
`hey` is a command line tool to contact DuckDuckGo Chat API from your terminal.
|
||||
based on [this article](https://blek.codes/blog/duckduckgo-ai-chat/)
|
||||
|
||||
demo:
|
||||
like this:
|
||||
|
||||
<p align=center><img src='hey-demo.gif' alt='a gif demostrating a prompt about a bedtime story' width=1000></p>
|
||||
```sh
|
||||
$ hey, how do you install windows on arch linux\?
|
||||
Contacting DuckDuckGo chat AI...
|
||||
Here are the basic steps to install Windows on a system that already has Arch Linux installed:
|
||||
|
||||
# disclaimer
|
||||
to clarify, as of may 17 2024, using a third party client [does not violate the ToS](https://duckduckgo.com/aichat/privacy-terms).
|
||||
1. Shrink the Arch Linux partition to make space for Windows. This can be done using a disk partitioning tool like GParted. You'll need at least 20-30GB of unallocated space for Windows.
|
||||
|
||||
by using this client, you acknowledge that you will be liable for any ToS violations as per GPLv3
|
||||
2. Download the Windows ISO file from Microsoft's website and write it to a USB drive to create a bootable Windows installer.
|
||||
|
||||
this project is not intended for API scraping purposes, and actually [has a soft protection against it](https://git.blek.codes/blek/hey/src/branch/main/src/main.rs#L34).
|
||||
3. Reboot the system and enter the BIOS/UEFI settings to change the boot order so that the USB drive is prioritized. This will allow you to boot into the Windows installer.
|
||||
|
||||
4. When the Windows installer loads, select the "Custom install" option and choose the unallocated space you created earlier as the location to install Windows.
|
||||
|
||||
5. Follow the on-screen instructions to complete the Windows installation. The installer will automatically format the partition and install Windows files.
|
||||
|
||||
6. Once installed, you'll need to reconfigure the bootloader like GRUB to add an entry to dual boot between Arch Linux and Windows. This can be done by running update-grub in Arch Linux.
|
||||
|
||||
7. Reboot and you should now see an option to choose between Arch Linux and Windows on startup. You can switch between them as needed.
|
||||
|
||||
A few things to note - make sure to backup any important data before shrinking partitions. Also, Windows may overwrite the MBR with its own bootloader, so reconfiguring GRUB is important to retain Arch Linux booting ability. With some preparation, it's possible to smoothly install Windows alongside an existing Arch Linux installation.
|
||||
```
|
||||
|
||||
# installation
|
||||
if you run linux or macos,
|
||||
|
@ -22,18 +35,12 @@ cargo b -r
|
|||
sudo cp target/release/hey /usr/bin/hey,
|
||||
```
|
||||
|
||||
if you are on windows, [download the binary file](#download-the-binary-file) or compile it yourself if you have the knowledge
|
||||
if you are on windows, idk have fun
|
||||
|
||||
## via a package manager
|
||||
|
||||
arch (AUR) - `paru -S hey-duck`
|
||||
|
||||
## download the binary file
|
||||
prebuilt binaries are available on [the releases page](https://git.blek.codes/blek/hey/releases) for macOS, Linux and Windows
|
||||
|
||||
## development version
|
||||
look around [on github actions](https://github.com/b1ek/hey/actions), select the latest one, scroll all the way down and then download an artifact for your platform
|
||||
|
||||
### note for packagers
|
||||
to avoid name conflicts, packages should be named `hey-duck` or its form in a different naming convention.
|
||||
please submit an issue or a PR if you have packaged this to a distro, or email one of the maintainers.
|
||||
|
@ -45,7 +52,7 @@ you can set their paths and filenames via `HEY_CONFIG_PATH`, `HEY_CONFIG_FILENAM
|
|||
|
||||
## config file reference
|
||||
```toml
|
||||
model = "Claude" # or "GPT4OMini"
|
||||
model = "claude-instant-1.2" # or "gpt-3.5-turbo-0125"
|
||||
tos = false # whether if you agree to ddg chat tos
|
||||
```
|
||||
|
||||
|
|
92
flake.lock
92
flake.lock
|
@ -1,92 +0,0 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1717067539,
|
||||
"narHash": "sha256-oIs5EF+6VpHJRvvpVWuqCYJMMVW/6h59aYUv9lABLtY=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "fa19d8c135e776dc97f4dcca08656a0eeb28d5c0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 0,
|
||||
"narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=",
|
||||
"path": "/nix/store/8s55w0927lh3mdbkxf434zb0c5hqsz8z-source",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1717646450,
|
||||
"narHash": "sha256-KE+UmfSVk5PG8jdKdclPVcMrUB8yVZHbsjo7ZT1Bm3c=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "818dbe2f96df233d2041739d6079bb616d3e5597",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
34
flake.nix
34
flake.nix
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
};
|
||||
|
||||
outputs = { self, flake-utils, naersk, nixpkgs }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = (import nixpkgs) {
|
||||
inherit system;
|
||||
};
|
||||
naersk' = pkgs.callPackage naersk { };
|
||||
in
|
||||
rec {
|
||||
defaultPackage = naersk'.buildPackage
|
||||
{
|
||||
src = ./.;
|
||||
buildInputs = with pkgs; [ openssl pkg-config ];
|
||||
};
|
||||
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
openssl
|
||||
pkg-config
|
||||
clippy
|
||||
rust-analyzer
|
||||
];
|
||||
nativeBuildInputs = with pkgs; [ rustc cargo ];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
{"version": 2, "width": 161, "height": 30, "timestamp": 1715916945, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}}
|
||||
[0.071719, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
|
||||
[0.072502, "o", "\u001b]2;blek@nyarch-947d:~\u0007\u001b]1;~\u0007"]
|
||||
[0.074115, "o", "\u001b]7;file://nyarch-947d/home/blek\u001b\\"]
|
||||
[0.074481, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36m~\u001b[00m \u001b[K"]
|
||||
[0.074574, "o", "\u001b[?1h\u001b=\u001b[?2004h"]
|
||||
[0.730106, "o", "h"]
|
||||
[1.057219, "o", "\bhe"]
|
||||
[1.281529, "o", "y"]
|
||||
[1.987643, "o", ","]
|
||||
[2.143429, "o", " "]
|
||||
[3.686669, "o", "t"]
|
||||
[3.810259, "o", "e"]
|
||||
[4.108094, "o", "l"]
|
||||
[4.252737, "o", "l"]
|
||||
[4.384378, "o", " "]
|
||||
[4.534988, "o", "m"]
|
||||
[4.634958, "o", "e"]
|
||||
[4.826033, "o", " "]
|
||||
[5.092297, "o", "a"]
|
||||
[5.252427, "o", " "]
|
||||
[5.506879, "o", "b"]
|
||||
[5.594621, "o", "e"]
|
||||
[5.824822, "o", "d"]
|
||||
[6.040323, "o", "t"]
|
||||
[6.174552, "o", "i"]
|
||||
[6.274551, "o", "m"]
|
||||
[6.376558, "o", "e"]
|
||||
[6.544205, "o", " "]
|
||||
[6.774768, "o", "s"]
|
||||
[6.85044, "o", "t"]
|
||||
[7.006644, "o", "o"]
|
||||
[7.107484, "o", "r"]
|
||||
[7.248646, "o", "y"]
|
||||
[7.962383, "o", "\u001b[?1l\u001b>\u001b[?2004l\r\r\n"]
|
||||
[7.964553, "o", "\u001b]2;hey, tell me a bedtime story\u0007\u001b]1;hey,\u0007"]
|
||||
[7.987539, "o", "\u001b[1;32mContacting DuckDuckGo chat AI...\u001b[0m\r\n"]
|
||||
[10.158228, "o", "Once upon a time, in a faraway land, there was a magical forest where all the animals lived in harmony. The wise old owl, who was the guardian of the forest, would tell stories to the young animals every night before they went to sleep.\r\n\r\n"]
|
||||
[10.533507, "o", "One night, as the moon shone brightly in the sky, the owl began to tell a special bedtime story. It was a story about a brave little squirrel named Sammy, who had a big dream to explore the world beyond the forest.\r\n\r\n"]
|
||||
[10.949932, "o", "Sammy was curious and adventurous, always eager to learn new things and meet new friends. One day, he decided to embark on a journey to the unknown, leaving the safety of the forest behind.\r\n\r\n"]
|
||||
[11.641422, "o", "As Sammy traveled through meadows and crossed rivers, he encountered many challenges and made new friends along the way. He met a friendly rabbit who showed him the beauty of the fields, a wise old turtle who taught him patience, and a playful butterfly who danced with him in the sunlight.\r\n\r\n"]
|
||||
[12.203232, "o", "Through his adventures, Sammy learned valuable lessons about courage, friendship, and the importance of following your dreams. And as he returned to the forest, he realized that home is not just a place, but a feeling of love and belonging.\r\n\r\n"]
|
||||
[12.932927, "o", "And so, under the watchful eyes of the wise old owl, the animals of the forest drifted off to sleep, their hearts filled with the magic of Sammy's journey and the warmth of the bedtime story. And as the night grew quiet, the stars twinkled above, whispering tales of wonder and dreams to all who listened.\r\n\r\n"]
|
||||
[13.280835, "o", "The end. Goodnight, dear friend. May your dreams be as magical as the stories of the forest.\r\n\r\n"]
|
||||
[13.284434, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
|
||||
[13.286063, "o", "\u001b]2;blek@nyarch-947d:~\u0007\u001b]1;~\u0007"]
|
||||
[13.289933, "o", "\u001b]7;file://nyarch-947d/home/blek\u001b\\"]
|
||||
[13.291292, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36m~\u001b[00m \u001b[K"]
|
||||
[80.829878, "o", ""]
|
BIN
hey-demo.gif
BIN
hey-demo.gif
Binary file not shown.
Before Width: | Height: | Size: 87 KiB |
176
src/api.rs
176
src/api.rs
|
@ -1,176 +0,0 @@
|
|||
|
||||
use std::error::Error;
|
||||
use std::process::exit;
|
||||
|
||||
use reqwest::{header::{HeaderMap, HeaderValue}, Client};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{config::Config, history::HistoryObject, session::Session};
|
||||
use crate::{WARN, RED, RESET};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatMessagePayload {
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl ChatMessagePayload {
|
||||
pub fn empty(role: String) -> Self {
|
||||
Self {
|
||||
role,
|
||||
content: String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatPayload {
|
||||
pub model: String,
|
||||
pub messages: Vec<ChatMessagePayload>
|
||||
}
|
||||
|
||||
impl ChatMessagePayload {
|
||||
pub fn is_ai(&self) -> bool {
|
||||
self.role == "assistant"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatChunk {
|
||||
pub role: Option<String>,
|
||||
pub message: String,
|
||||
pub created: u64,
|
||||
pub action: String,
|
||||
pub id: Option<String>,
|
||||
pub model: Option<String>
|
||||
}
|
||||
|
||||
impl Into<ChatMessagePayload> for ChatChunk {
|
||||
fn into(self) -> ChatMessagePayload {
|
||||
ChatMessagePayload {
|
||||
role: "assistant".to_string(),
|
||||
content: self.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ErrChatChunk {
|
||||
pub action: String,
|
||||
pub status: u64,
|
||||
#[serde(rename = "type")]
|
||||
pub err_type: String,
|
||||
}
|
||||
|
||||
fn get_headers() -> HeaderMap {
|
||||
let mut map = HeaderMap::new();
|
||||
map.insert("Host", HeaderValue::from_static("duckduckgo.com"));
|
||||
map.insert("Accept", HeaderValue::from_static("text/event-stream"));
|
||||
map.insert("Accept-Language", HeaderValue::from_static("en-US,en;q=0.5"));
|
||||
map.insert("Accept-Encoding", HeaderValue::from_static("gzip, deflate, br"));
|
||||
map.insert("Referer", HeaderValue::from_static("https://duckduckgo.com/"));
|
||||
map.insert("User-Agent", HeaderValue::from_static("Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0"));
|
||||
map.insert("DNT", HeaderValue::from_static("1"));
|
||||
map.insert("Sec-GPC", HeaderValue::from_static("1"));
|
||||
map.insert("Connection", HeaderValue::from_static("keep-alive"));
|
||||
map.insert("Cookie", HeaderValue::from_static("dcm=3; ay=b"));
|
||||
map.insert("Sec-Fetch-Dest", HeaderValue::from_static("empty"));
|
||||
map.insert("Sec-Fetch-Mode", HeaderValue::from_static("cors"));
|
||||
map.insert("Sec-Fetch-Site", HeaderValue::from_static("same-origin"));
|
||||
map.insert("TE", HeaderValue::from_static("trailers"));
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
pub async fn simulate_browser_reqs(cli: &Client) -> Result<(), Box<dyn Error>> {
|
||||
let req = cli.get("https://duckduckgo.com/country.json")
|
||||
.headers(get_headers())
|
||||
.header("X-Requested-With", "XMLHttpRequest")
|
||||
.build()?;
|
||||
cli.execute(req).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_vqd(cli: &Client) -> Result<String, Box<dyn Error>> {
|
||||
|
||||
let mut headers = get_headers();
|
||||
headers.insert("Cache-Control", HeaderValue::from_static("no-store"));
|
||||
headers.insert("x-vqd-accept", HeaderValue::from_static("1"));
|
||||
|
||||
let req = cli.get("https://duckduckgo.com/duckchat/v1/status")
|
||||
.headers(headers)
|
||||
.build()?;
|
||||
|
||||
let res = cli.execute(req).await?;
|
||||
|
||||
let data = res.headers().iter().find(|x| x.0 == "x-vqd-4").map(|x| x.1.clone());
|
||||
if let Some(data) = data {
|
||||
Ok(data.to_str()?.to_string())
|
||||
} else {
|
||||
Err("No VQD header returned".into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_res<'a>(cli: &Client, query: String, vqd: String, config: &Config) {
|
||||
let init_msg = ChatMessagePayload { role: "user".into(), content: query };
|
||||
|
||||
let mut session = Session::create_or_restore(&vqd);
|
||||
session.push_message(&init_msg).unwrap();
|
||||
|
||||
let payload = ChatPayload {
|
||||
model: config.model.to_string(),
|
||||
messages: session.get_messages()
|
||||
};
|
||||
let payload = serde_json::to_string(&payload).unwrap();
|
||||
|
||||
println!("{}", init_msg.display_as_history());
|
||||
|
||||
let req = cli.post("https://duckduckgo.com/duckchat/v1/chat")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0")
|
||||
.header("x-vqd-4", vqd.clone())
|
||||
.body(payload)
|
||||
.build().unwrap();
|
||||
|
||||
let mut res = cli.execute(req).await.unwrap();
|
||||
let new_vqd = res.headers().iter().find(|x| x.0 == "x-vqd-4");
|
||||
let vqd_set_res =
|
||||
if let Some(new_vqd) = new_vqd {
|
||||
session.set_last_vqd(new_vqd.1.as_bytes().iter().map(|x| char::from(*x)).collect::<String>())
|
||||
} else {
|
||||
eprintln!("{WARN}Warn: DuckDuckGo did not return new VQD. Ignore this if everything else is ok.{RESET}");
|
||||
session.set_last_vqd(vqd.clone())
|
||||
};
|
||||
if let Err(err) = vqd_set_res {
|
||||
eprintln!("{WARN}Warn: Could not save VQD to cache: {err}{RESET}");
|
||||
}
|
||||
|
||||
let mut error = None;
|
||||
println!("{}", ChatMessagePayload::empty("assistant".into()).display_as_history());
|
||||
while let Some(chunk) = res.chunk().await.unwrap() {
|
||||
|
||||
if let Ok(obj) = serde_json::from_slice::<ErrChatChunk>(&chunk) {
|
||||
if obj.action == "error" {
|
||||
eprintln!("{RED}Error obtaining response: {} - {}{RESET}", obj.status, obj.err_type);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let chunk = String::from_utf8(chunk.to_vec()).unwrap();
|
||||
let chunk = chunk.replace("data: ", "");
|
||||
for line in chunk.lines() {
|
||||
if let Ok(obj) = serde_json::from_str::<ChatChunk>(line) {
|
||||
if let Err(err) = session.push_ai_message_chunk(&obj) {
|
||||
error = Some(err);
|
||||
}
|
||||
print!("{}", obj.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\n");
|
||||
|
||||
if let Some(err) = error {
|
||||
eprintln!("Error while writing to session: {err:#?}");
|
||||
eprintln!("Session may be broken.");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
|
||||
use std::{env, error::Error, fs, io, path::PathBuf};
|
||||
use home::home_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Cache {
|
||||
pub last_vqd: String,
|
||||
pub last_vqd_time: u64
|
||||
}
|
||||
|
||||
impl Default for Cache {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_vqd: "".into(),
|
||||
last_vqd_time: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
|
||||
pub fn get_path<T: From<String>>() -> T {
|
||||
match env::var("HEY_CACHE_PATH") {
|
||||
Ok(v) => v,
|
||||
Err(_) =>
|
||||
match home_dir() {
|
||||
Some(home) => home.join(".cache/hey").as_os_str().as_encoded_bytes().iter().map(|x| char::from(*x)).collect::<String>(),
|
||||
None => panic!("Cannot detect your home directory!")
|
||||
}
|
||||
}.into()
|
||||
}
|
||||
|
||||
pub fn get_file_name<T: From<String>>() -> T {
|
||||
match env::var("HEY_CACHE_FILENAME") {
|
||||
Ok(v) => v,
|
||||
Err(_) => "cache.json".into()
|
||||
}.into()
|
||||
}
|
||||
|
||||
fn ensure_dir_exists() -> io::Result<()> {
|
||||
let path: PathBuf = Self::get_path();
|
||||
if ! path.is_dir() { fs::create_dir_all(path)? }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load() -> Result<Self, Box<dyn Error>> {
|
||||
let path: PathBuf = Self::get_path();
|
||||
Self::ensure_dir_exists()?;
|
||||
|
||||
let file_path = path.join(Self::get_file_name::<PathBuf>());
|
||||
if ! file_path.is_file() {
|
||||
let def = Self::default();
|
||||
def.save()?;
|
||||
Ok(def)
|
||||
} else {
|
||||
let file = fs::read_to_string(file_path)?;
|
||||
Ok(serde_json::from_str(&file)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(self: &Self) -> Result<(), Box<dyn Error>> {
|
||||
let path: PathBuf = Self::get_path();
|
||||
Self::ensure_dir_exists()?;
|
||||
|
||||
let file_path = path.join(Self::get_file_name::<PathBuf>());
|
||||
fs::write(file_path, serde_json::to_string_pretty(self)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_last_vqd<T: Into<String>>(self: &mut Self, vqd: T) -> Result<(), Box<dyn Error>> {
|
||||
self.last_vqd = vqd.into();
|
||||
self.last_vqd_time = chrono::Local::now().timestamp_millis() as u64;
|
||||
self.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_last_vqd<'a, T: From<&'a String>>(self: &'a Self) -> Option<T> {
|
||||
if self.last_vqd_time - (chrono::Local::now().timestamp_millis() as u64) < 60000 {
|
||||
Some((&self.last_vqd).into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +1,14 @@
|
|||
use crate::{RED, BLUE, WARN, RESET, GRAY};
|
||||
|
||||
use std::{env, error::Error, fs, io, path::PathBuf, process::exit};
|
||||
use std::{env, error::Error, fs, io, path::PathBuf};
|
||||
|
||||
use home::home_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Model {
|
||||
// outdated
|
||||
Claude12,
|
||||
GPT35,
|
||||
GPT3,
|
||||
|
||||
// current
|
||||
Claude,
|
||||
GPT4OMini,
|
||||
Llama,
|
||||
Mixtral
|
||||
GPT3,
|
||||
}
|
||||
|
||||
impl ToString for Model {
|
||||
|
@ -27,9 +19,6 @@ impl ToString for Model {
|
|||
|
||||
Self::Claude => String::from("claude-3-haiku-20240307"),
|
||||
Self::GPT3 => String::from("gpt-3.5-turbo-0125"),
|
||||
Self::Llama => String::from("meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo"),
|
||||
Self::Mixtral => String::from("mistralai/Mixtral-8x7B-Instruct-v0.1"),
|
||||
Self::GPT4OMini => String::from("gpt-4o-mini")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,27 +39,6 @@ impl Default for Config {
|
|||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
pub fn set_tos(&mut self, tos: bool) -> Result<(), Box<dyn Error>> {
|
||||
self.tos = tos;
|
||||
self.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_tos(&self) -> () {
|
||||
if ! self.tos {
|
||||
eprintln!("{RED}You need to agree to DuckDuckgo AI Chat TOS to continue.{RESET}");
|
||||
eprintln!("{GRAY}Visit it on this URL: {RESET}{BLUE}https://duck.ai{RESET}");
|
||||
eprintln!();
|
||||
eprintln!("{GRAY}If it doesnt pop up, open the URL in a private window.{RESET}");
|
||||
eprintln!("{GRAY}Running this command is equivalent to clicking \"I Agree\" in the ToS window:{RESET}");
|
||||
eprintln!("{BLUE} {} --agree-tos{RESET}", std::env::current_exe().unwrap().display());
|
||||
eprintln!();
|
||||
eprintln!("{WARN}Note: if you want to, modify `tos` parameter in {}{RESET}", Config::get_path::<PathBuf>().join(Config::get_file_name::<String>()).display());
|
||||
exit(3);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_path<T: From<String>>() -> T {
|
||||
match env::var("HEY_CONFIG_PATH") {
|
||||
Ok(v) => v,
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
use crate::api::ChatMessagePayload;
|
||||
use crate::{RESET, GREEN, GRAY, BLUE};
|
||||
|
||||
pub trait HistoryObject {
|
||||
fn display_as_history(&self) -> String;
|
||||
}
|
||||
|
||||
impl HistoryObject for ChatMessagePayload {
|
||||
fn display_as_history(&self) -> String {
|
||||
let user = &self.role;
|
||||
let msg = &self.content;
|
||||
|
||||
let role_color = {
|
||||
if user == "user" { GREEN }
|
||||
else if user == "assistant" { BLUE }
|
||||
else { GRAY }
|
||||
};
|
||||
|
||||
let short_user = {
|
||||
if user == "user" { "you" }
|
||||
else if user == "assistant" { "ai" }
|
||||
else { user }
|
||||
}.to_string();
|
||||
|
||||
format!("{role_color}{short_user}{RESET}\t: {msg}{RESET}")
|
||||
}
|
||||
}
|
191
src/main.rs
191
src/main.rs
|
@ -1,33 +1,153 @@
|
|||
use std::process::exit;
|
||||
use std::{error::Error, path::PathBuf, process::exit};
|
||||
|
||||
use reqwest::Client;
|
||||
use reqwest::{header::{HeaderMap, HeaderValue}, Client};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use clap::Parser;
|
||||
use session::Session;
|
||||
use std::io::{stdout, IsTerminal};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::api::{get_res, get_vqd, simulate_browser_reqs};
|
||||
use crate::{cache::Cache, config::Config};
|
||||
|
||||
mod cache;
|
||||
mod config;
|
||||
mod api;
|
||||
mod session;
|
||||
mod history;
|
||||
|
||||
pub const GREEN: &str = "\x1b[1;32m";
|
||||
pub const RED: &str = "\x1b[1;31m";
|
||||
pub const BLUE: &str = "\x1b[34m";
|
||||
pub const WARN: &str = "\x1b[33m";
|
||||
pub const RESET: &str = "\x1b[0m";
|
||||
pub const GRAY: &str = "\x1b[90m";
|
||||
const GREEN: &str = "\x1b[1;32m";
|
||||
const RED: &str = "\x1b[1;31m";
|
||||
const BLUE: &str = "\x1b[34m";
|
||||
const WARN: &str = "\x1b[33m";
|
||||
const RESET: &str = "\x1b[0m";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ChatMessagePayload {
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ChatPayload {
|
||||
pub model: String,
|
||||
pub messages: Vec<ChatMessagePayload>
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ChatChunk {
|
||||
pub role: Option<String>,
|
||||
pub message: String,
|
||||
pub created: u64,
|
||||
pub action: String,
|
||||
pub id: Option<String>,
|
||||
pub model: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ErrChatChunk {
|
||||
pub action: String,
|
||||
pub status: u64,
|
||||
#[serde(rename = "type")]
|
||||
pub err_type: String,
|
||||
}
|
||||
|
||||
fn get_headers() -> HeaderMap {
|
||||
let mut map = HeaderMap::new();
|
||||
map.insert("Host", HeaderValue::from_static("duckduckgo.com"));
|
||||
map.insert("Accept", HeaderValue::from_static("text/event-stream"));
|
||||
map.insert("Accept-Language", HeaderValue::from_static("en-US,en;q=0.5"));
|
||||
map.insert("Accept-Encoding", HeaderValue::from_static("gzip, deflate, br"));
|
||||
map.insert("Referer", HeaderValue::from_static("https://duckduckgo.com/"));
|
||||
map.insert("User-Agent", HeaderValue::from_static("Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0"));
|
||||
map.insert("DNT", HeaderValue::from_static("1"));
|
||||
map.insert("Sec-GPC", HeaderValue::from_static("1"));
|
||||
map.insert("Connection", HeaderValue::from_static("keep-alive"));
|
||||
map.insert("Cookie", HeaderValue::from_static("dcm=3; ay=b"));
|
||||
map.insert("Sec-Fetch-Dest", HeaderValue::from_static("empty"));
|
||||
map.insert("Sec-Fetch-Mode", HeaderValue::from_static("cors"));
|
||||
map.insert("Sec-Fetch-Site", HeaderValue::from_static("same-origin"));
|
||||
map.insert("TE", HeaderValue::from_static("trailers"));
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
async fn simulate_browser_reqs(cli: &Client) -> Result<(), Box<dyn Error>> {
|
||||
let req = cli.get("https://duckduckgo.com/country.json")
|
||||
.headers(get_headers())
|
||||
.header("X-Requested-With", "XMLHttpRequest")
|
||||
.build()?;
|
||||
cli.execute(req).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_vqd(cli: &Client) -> Result<String, Box<dyn Error>> {
|
||||
|
||||
let mut headers = get_headers();
|
||||
headers.insert("Cache-Control", HeaderValue::from_static("no-store"));
|
||||
headers.insert("x-vqd-accept", HeaderValue::from_static("1"));
|
||||
|
||||
let req = cli.get("https://duckduckgo.com/duckchat/v1/status")
|
||||
.headers(headers)
|
||||
.build()?;
|
||||
|
||||
let res = cli.execute(req).await?;
|
||||
|
||||
let data = res.headers().iter().find(|x| x.0 == "x-vqd-4").map(|x| x.1.clone());
|
||||
if let Some(data) = data {
|
||||
Ok(data.to_str()?.to_string())
|
||||
} else {
|
||||
Err("No VQD header returned".into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_res<'a>(cli: &Client, query: String, vqd: String, cache: &'a mut Cache, config: &Config) {
|
||||
let payload = ChatPayload {
|
||||
model: config.model.to_string(),
|
||||
messages: vec![ ChatMessagePayload { role: "user".into(), content: query } ]
|
||||
};
|
||||
let payload = serde_json::to_string(&payload).unwrap();
|
||||
|
||||
let req = cli.post("https://duckduckgo.com/duckchat/v1/chat")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0")
|
||||
.header("x-vqd-4", vqd.clone())
|
||||
.body(payload)
|
||||
.build().unwrap();
|
||||
|
||||
let mut res = cli.execute(req).await.unwrap();
|
||||
let new_vqd = res.headers().iter().find(|x| x.0 == "x-vqd-4");
|
||||
let vqd_set_res =
|
||||
if let Some(new_vqd) = new_vqd {
|
||||
cache.set_last_vqd(new_vqd.1.as_bytes().iter().map(|x| char::from(*x)).collect::<String>())
|
||||
} else {
|
||||
eprintln!("{WARN}Warn: DuckDuckGo did not return new VQD. Ignore this if everything else is ok.{RESET}");
|
||||
cache.set_last_vqd(vqd.clone())
|
||||
};
|
||||
if let Err(err) = vqd_set_res {
|
||||
eprintln!("{WARN}Warn: Could not save VQD to cache: {err}{RESET}");
|
||||
}
|
||||
|
||||
while let Some(chunk) = res.chunk().await.unwrap() {
|
||||
|
||||
if let Ok(obj) = serde_json::from_slice::<ErrChatChunk>(&chunk) {
|
||||
if obj.action == "error" {
|
||||
eprintln!("{RED}Error obtaining response: {} - {}{RESET}", obj.status, obj.err_type);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let chunk = String::from_utf8(chunk.to_vec()).unwrap();
|
||||
let chunk = chunk.replace("data: ", "");
|
||||
for line in chunk.lines() {
|
||||
if let Ok(obj) = serde_json::from_str::<ChatChunk>(line) {
|
||||
print!("{}", obj.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\n");
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[arg(long, default_value = "false", required = false, help = "If you want to agree to the DuckDuckGo TOS")]
|
||||
pub agree_tos: bool,
|
||||
#[arg(long, short, default_value = "false", required = false, help = "Clear current session")]
|
||||
pub clear: bool,
|
||||
#[arg()]
|
||||
pub query: Vec<String>,
|
||||
}
|
||||
|
@ -43,44 +163,37 @@ async fn main() {
|
|||
}
|
||||
|
||||
let args = Args::parse();
|
||||
let query = args.query.join(" ").trim().to_string();
|
||||
|
||||
if args.clear {
|
||||
Session::clear();
|
||||
println!("Cleared session");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
{
|
||||
let session = Session::create_or_restore("");
|
||||
if session.is_restored() {
|
||||
session.print_history();
|
||||
}
|
||||
}
|
||||
|
||||
if query.len() == 0 {
|
||||
exit(0);
|
||||
}
|
||||
let query = args.query.join(" ");
|
||||
|
||||
let mut cache = Cache::load().unwrap();
|
||||
let mut config = Config::load().unwrap();
|
||||
|
||||
if args.agree_tos {
|
||||
if ! config.tos {
|
||||
println!("{GREEN}TOS accepted{RESET}");
|
||||
}
|
||||
config.set_tos(true).expect("Error saving config");
|
||||
config.tos = true;
|
||||
config.save().expect("Error saving config");
|
||||
}
|
||||
|
||||
config.check_tos();
|
||||
if ! config.tos {
|
||||
eprintln!("{RED}You need to agree to duckduckgo AI chat TOS to continue.{RESET}");
|
||||
eprintln!("{RED}Visit it on this URL: {RESET}{BLUE}https://duckduckgo.com/?q=duckduckgo+ai+chat&ia=chat{RESET}");
|
||||
eprintln!("Once you read it, pass --agree-tos parameter to agree.");
|
||||
eprintln!("{WARN}Note: if you want to, modify `tos` parameter in {}{RESET}", Config::get_path::<PathBuf>().join(Config::get_file_name::<String>()).display());
|
||||
exit(3);
|
||||
}
|
||||
|
||||
println!("{GREEN}Contacting DuckDuckGo chat AI...{RESET}");
|
||||
|
||||
let cli = Client::new();
|
||||
simulate_browser_reqs(&cli).await.unwrap();
|
||||
|
||||
let vqd = match Session::restore_vqd() {
|
||||
Some(v) => { v },
|
||||
let vqd = match cache.get_last_vqd() {
|
||||
Some(v) => { println!("using cached vqd"); v},
|
||||
None => get_vqd(&cli).await.unwrap()
|
||||
};
|
||||
|
||||
get_res(&cli, query, vqd, &config).await;
|
||||
get_res(&cli, query, vqd, &mut cache, &config).await;
|
||||
|
||||
}
|
||||
|
|
189
src/session.rs
189
src/session.rs
|
@ -1,189 +0,0 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Local};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::{ChatChunk, ChatMessagePayload};
|
||||
use crate::{GRAY, RESET};
|
||||
use crate::history::HistoryObject;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Session {
|
||||
session_id: String,
|
||||
ttl: DateTime<Local>,
|
||||
messages: Vec<ChatMessagePayload>,
|
||||
vqd: String,
|
||||
|
||||
#[serde(skip)]
|
||||
restored: bool
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn terminal_session_id<T: From<String>>() -> T {
|
||||
#[cfg(unix)]
|
||||
fn inner() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let child = Command::new("tty")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
let child = child.wait_with_output()?;
|
||||
if ! child.status.success() {
|
||||
Err("")? // value ignored
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(child.stdout)?)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn inner() -> Result<String, ()> {
|
||||
Err(())
|
||||
}
|
||||
|
||||
inner()
|
||||
.unwrap_or("default".into())
|
||||
.replace(['/', '\\', ':'], "-")
|
||||
.trim()
|
||||
.trim_start_matches('-')
|
||||
.to_string()
|
||||
.into()
|
||||
}
|
||||
|
||||
fn path_for_id<T: Into<String>>(id: T) -> PathBuf {
|
||||
let id: String = id.into();
|
||||
std::env::temp_dir().join(format!("hey-duck-session-{id}"))
|
||||
}
|
||||
|
||||
fn path(&self) -> PathBuf {
|
||||
Self::path_for_id(&self.session_id)
|
||||
}
|
||||
|
||||
fn restore_with_id<T: Into<String>>(id: T) -> Option<Self> {
|
||||
fn inner(path: PathBuf) -> Result<Session, Box<dyn std::error::Error>> {
|
||||
if path.is_file() {
|
||||
let data = fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&data)?)
|
||||
} else {
|
||||
Err("")? // value ignored
|
||||
}
|
||||
}
|
||||
|
||||
match inner(Self::path_for_id(id)) {
|
||||
Ok(mut session) => {
|
||||
session.restored = true;
|
||||
if session.is_expired() {
|
||||
session.destroy().expect("Couldn't destroy expired session");
|
||||
None
|
||||
} else {
|
||||
session.increase_ttl().expect("Couldn't increase TTL");
|
||||
Some(session)
|
||||
}
|
||||
},
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ttl() -> DateTime<Local> {
|
||||
Local::now() + Duration::from_secs(60 * 5)
|
||||
}
|
||||
|
||||
pub fn increase_ttl(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.ttl = Self::get_ttl();
|
||||
Ok(self.save()?)
|
||||
}
|
||||
|
||||
fn new<I: Into<String>, V: Into<String>>(id: I, vqd: V) -> Self {
|
||||
Self {
|
||||
session_id: id.into(),
|
||||
ttl: Self::get_ttl(),
|
||||
messages: vec![],
|
||||
vqd: vqd.into(),
|
||||
|
||||
restored: false
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if self.messages.len() == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
fs::write(self.path(), serde_json::to_string_pretty(self)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_expired(&self) -> bool {
|
||||
self.ttl < Local::now()
|
||||
}
|
||||
|
||||
pub fn clear() {
|
||||
let session = Session::create_or_restore("");
|
||||
if session.is_restored() {
|
||||
session.destroy().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_restored(&self) -> bool {
|
||||
self.restored
|
||||
}
|
||||
|
||||
pub fn print_history(&self) {
|
||||
println!("{GRAY}* start restored conversation *{RESET}");
|
||||
println!();
|
||||
|
||||
for message in self.messages.iter() {
|
||||
println!("{}", message.display_as_history());
|
||||
if message.role == "assistant" {
|
||||
println!("\n");
|
||||
}
|
||||
}
|
||||
|
||||
println!("{GRAY}* end restored conversation *{RESET}");
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(fs::remove_file(self.path())?)
|
||||
}
|
||||
|
||||
pub fn create_or_restore<T: Into<String>>(vqd: T) -> Self {
|
||||
let session_id: String = Self::terminal_session_id();
|
||||
match Self::restore_with_id(&session_id) {
|
||||
Some(session) => { session },
|
||||
None => {
|
||||
let session = Self::new(&session_id, vqd);
|
||||
session.save().expect("Couldn't save new session");
|
||||
session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_message(&mut self, msg: &ChatMessagePayload) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.messages.push(msg.clone());
|
||||
Ok(self.save()?)
|
||||
}
|
||||
|
||||
pub fn push_ai_message_chunk(&mut self, chunk: &ChatChunk) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if self.messages.last().unwrap().is_ai() {
|
||||
self.messages.last_mut().unwrap().content += chunk.message.as_str();
|
||||
} else {
|
||||
self.messages.push(chunk.clone().into());
|
||||
}
|
||||
|
||||
Ok(self.save()?)
|
||||
}
|
||||
|
||||
pub fn get_messages(&self) -> Vec<ChatMessagePayload> {
|
||||
self.messages.clone()
|
||||
}
|
||||
|
||||
pub fn restore_vqd() -> Option<String> {
|
||||
Self::restore_with_id(Self::terminal_session_id::<String>()).map(|x| x.vqd)
|
||||
}
|
||||
|
||||
pub fn set_last_vqd<T: Into<String>>(&mut self, vqd: T) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.vqd = vqd.into();
|
||||
Ok(self.save()?)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue