Compare commits

...

19 Commits
0.1.1 ... main

Author SHA1 Message Date
aa0 ffa2bc9bd2
Remove commented code in `get_last_vqd`. 2024-08-05 06:54:25 +01:00
Ahmad-A0 c49bbd2692 Update README and config.rs to mark GPT4o as the newer model 2024-08-04 02:20:51 +01:00
Ahmad-A0 f0d3ef36e2 Disable retrieving the vqd, as it seems to break things 2024-08-04 02:12:50 +01:00
Ahmad-A0 (aider) 146ea25974 fix: Update GPT4OMini model identifier 2024-08-04 01:41:58 +01:00
Ahmad-A0 (aider) 1d9a05fb51 feat: Add gpt-4o-mini model to config 2024-08-04 01:41:15 +01:00
blek 1baf800e3d Merge pull request 'add flake' (#3) from loafey/hey:main into main
Reviewed-on: #3
2024-06-10 12:59:03 +02:00
loafey 04ea44dcb7 add flake 2024-06-07 17:06:24 +02:00
b1ek 6dc8ef16b2
add llama and mixtral models 2024-06-05 09:36:33 +10:00
b1ek 28185c57a2
run github workflow only if an actual change has happened 2024-05-19 22:53:20 +10:00
b1ek ee73d00766
fix misleading runner name 2024-05-19 22:48:48 +10:00
iwannet69 01719d3ad8
mention github actions in readme 2024-05-19 22:48:06 +10:00
iwannet69 85903870c4
Create rust.yml 2024-05-19 22:38:48 +10:00
b1ek c0392efb5f
mention prebuilt binaries in releases 2024-05-19 22:37:33 +10:00
b1ek 9206ad4a6b
add a disclaimer 2024-05-17 14:53:40 +10:00
b1ek 037a74ca0f
add asciinema source file 2024-05-17 14:35:36 +10:00
b1ek ef3772bf47
add an asciinema gif preview 2024-05-17 14:33:37 +10:00
b1ek a07c9f14eb
exit when agreeing to TOS 2024-05-15 15:49:44 +10:00
b1ek 4c4258fbdc
move out all the api calls to a different file 2024-05-15 15:48:25 +10:00
b1ek 7384fec6ba
update config file reference 2024-05-05 17:01:47 +10:00
10 changed files with 419 additions and 164 deletions

71
.github/workflows/rust.yml vendored Normal file
View File

@ -0,0 +1,71 @@
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

View File

@ -2,29 +2,16 @@
`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/)
like this:
demo:
```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:
<p align=center><img src='hey-demo.gif' alt='a gif demostrating a prompt about a bedtime story' width=1000></p>
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.
# disclaimer
to clarify, as of may 17 2024, using a third party client [does not violate the ToS](https://duckduckgo.com/aichat/privacy-terms).
2. Download the Windows ISO file from Microsoft's website and write it to a USB drive to create a bootable Windows installer.
by using this client, you acknowledge that you will be liable for any ToS violations as per GPLv3
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.
```
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).
# installation
if you run linux or macos,
@ -35,12 +22,18 @@ cargo b -r
sudo cp target/release/hey /usr/bin/hey,
```
if you are on windows, idk have fun
if you are on windows, [download the binary file](#download-the-binary-file) or compile it yourself if you have the knowledge
## 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.
@ -52,11 +45,11 @@ you can set their paths and filenames via `HEY_CONFIG_PATH`, `HEY_CONFIG_FILENAM
## config file reference
```toml
model = "claude-instant-1.2" # or "gpt-3.5-turbo-0125"
model = "Claude" # or "GPT4OMini"
tos = false # whether if you agree to ddg chat tos
```
## cache file reference
cache file stores the last VQD used. it is (probably) there so that the ai model would remember your history. [read more about duckduckgo chat api](https://blek.codes/blog/duckduckgo-ai-chat/)
if you want to reset the VQD, just delete the file.
if you want to reset the VQD, just delete the file.

92
flake.lock Normal file
View File

@ -0,0 +1,92 @@
{
"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 Normal file
View File

@ -0,0 +1,34 @@
{
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 ];
};
}
);
}

49
hey-demo.cast Normal file
View File

@ -0,0 +1,49 @@
{"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 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

135
src/api.rs Normal file
View File

@ -0,0 +1,135 @@
use std::error::Error;
use std::process::exit;
use reqwest::{header::{HeaderMap, HeaderValue}, Client};
use serde::{Deserialize, Serialize};
use crate::{cache::Cache, config::Config};
use crate::{WARN, RED, RESET};
#[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
}
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, 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");
}

View File

@ -76,10 +76,6 @@ impl Cache {
}
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
}
None
}
}
}

View File

@ -5,10 +5,16 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Model {
// outdated
Claude12,
GPT35,
Claude,
GPT3,
// current
Claude,
GPT4OMini,
Llama,
Mixtral
}
impl ToString for Model {
@ -19,6 +25,9 @@ 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/Llama-3-70b-chat-hf"),
Self::Mixtral => String::from("mistralai/Mixtral-8x7B-Instruct-v0.1"),
Self::GPT4OMini => String::from("gpt-4o-mini")
}
}
}
@ -85,4 +94,4 @@ impl Config {
Ok(conf)
}
}
}
}

View File

@ -1,147 +1,22 @@
use std::{error::Error, path::PathBuf, process::exit};
use std::{path::PathBuf, process::exit};
use reqwest::{header::{HeaderMap, HeaderValue}, Client};
use serde::{Deserialize, Serialize};
use reqwest::Client;
use clap::Parser;
use std::io::{stdout, IsTerminal};
use crate::{cache::Cache, config::Config};
use crate::api::{get_res, get_vqd, simulate_browser_reqs};
mod cache;
mod config;
mod api;
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");
}
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";
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
@ -174,6 +49,7 @@ async fn main() {
}
config.tos = true;
config.save().expect("Error saving config");
exit(0);
}
if ! config.tos {