Compare commits
81 commits
Author | SHA1 | Date | |
---|---|---|---|
2eed8fc323 | |||
a691732a81 | |||
c4cf11a5ef | |||
a8c86790fc | |||
f955d63937 | |||
5b1e215b77 | |||
b101b5af51 | |||
86c6c96909 | |||
cd493f8d32 | |||
74b29455e5 | |||
47477732ba | |||
cbf35e9746 | |||
adf97d4b20 | |||
328ec0bfdc | |||
199bce4f90 | |||
9c9172d4c9 | |||
4f6063bf72 | |||
b7b466b774 | |||
25644b9d4e | |||
849393fbd1 | |||
c2f210c32c | |||
7a57c7bf1e | |||
fe539a637e | |||
f11aa99038 | |||
c39a425d48 | |||
430e0fce85 | |||
7fbe876c54 | |||
3ed54bf7e8 | |||
1370bc8a43 | |||
0725850ad0 | |||
de1d9931b1 | |||
24067eca99 | |||
3696d4cb6d | |||
f06b84bf66 | |||
43aae463e8 | |||
87458f30b6 | |||
64a771f8cc | |||
bac21898c9 | |||
245744a317 | |||
109e20c7b4 | |||
0c10b15447 | |||
b86fd6b6bc | |||
6fa292d1de | |||
b8a338d9ba | |||
c2e28c3791 | |||
050d45ee2e | |||
39dfa6816e | |||
03314a53d1 | |||
1c6edf2039 | |||
95ba628934 | |||
96478fb5d2 | |||
74c65d993d | |||
9237d1a048 | |||
4be68e4ba1 | |||
2463986e8d | |||
38ef5a45f6 | |||
0f6f0deb9c | |||
9c159c7170 | |||
d341c66390 | |||
c754338bf4 | |||
56d07057c9 | |||
384464bdbc | |||
d45f13f030 | |||
927ce9d3ed | |||
17054c0a9c | |||
b6f5ae177a | |||
83053c57a8 | |||
1ad6dad9eb | |||
7de4cdc0d6 | |||
ce60a00868 | |||
9f87a327e0 | |||
f3ee26f24b | |||
d6509cccbe | |||
1dc0a399a0 | |||
30b3c2352e | |||
949a49e5de | |||
a0b31d3326 | |||
a16782617a | |||
73463a8d21 | |||
906c2ed8df | |||
b1202cc889 |
75 changed files with 4696 additions and 319 deletions
16
.forgejo/build-service/action.yml
Normal file
16
.forgejo/build-service/action.yml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
inputs:
|
||||||
|
service-name:
|
||||||
|
description: 'name of the service to build and upload'
|
||||||
|
required: true
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
mkdir -pv artifacts
|
||||||
|
cargo build --release --bin ${{ inputs.service-name }}
|
||||||
|
mv target/release/${{ inputs.service-name }} artifacts/
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: "${{ inputs.service-name }}"
|
||||||
|
path: artifacts/
|
39
.forgejo/workflows/build-all.yml
Normal file
39
.forgejo/workflows/build-all.yml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
build-all-services:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: rust:1-bookworm
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq nodejs git clang
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- id: vorebot
|
||||||
|
uses: ./.forgejo/build-service
|
||||||
|
with:
|
||||||
|
service-name: "vorebot"
|
||||||
|
- id: asklyphe-auth-frontend
|
||||||
|
uses: ./.forgejo/build-service
|
||||||
|
with:
|
||||||
|
service-name: "asklyphe-auth-frontend"
|
||||||
|
- id: asklyphe-frontend
|
||||||
|
uses: ./.forgejo/build-service
|
||||||
|
with:
|
||||||
|
service-name: "asklyphe-frontend"
|
||||||
|
- id: authservice
|
||||||
|
uses: ./.forgejo/build-service
|
||||||
|
with:
|
||||||
|
service-name: "authservice"
|
||||||
|
- id: bingservice
|
||||||
|
uses: ./.forgejo/build-service
|
||||||
|
with:
|
||||||
|
service-name: "bingservice"
|
||||||
|
- id: googleservice
|
||||||
|
uses: ./.forgejo/build-service
|
||||||
|
with:
|
||||||
|
service-name: "googleservice"
|
||||||
|
- id: lyphedb
|
||||||
|
uses: ./.forgejo/build-service
|
||||||
|
with:
|
||||||
|
service-name: "lyphedb"
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,2 +1,7 @@
|
||||||
.idea
|
.idea
|
||||||
/target
|
/target
|
||||||
|
# /nginx
|
||||||
|
database
|
||||||
|
database.bak
|
||||||
|
.env
|
||||||
|
proxies.txt
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
http://127.0.0.1:8001 {
|
http://127.0.0.1:1235 {
|
||||||
route /static/* {
|
route /static/* {
|
||||||
uri strip_prefix /static
|
uri strip_prefix /static
|
||||||
file_server {
|
file_server {
|
||||||
|
@ -6,10 +6,10 @@ http://127.0.0.1:8001 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reverse_proxy 127.0.0.1:5843
|
reverse_proxy http://auth-frontend:5843
|
||||||
}
|
}
|
||||||
|
|
||||||
http://127.0.0.1:8002 {
|
http://127.0.0.1:1234 {
|
||||||
route /static/* {
|
route /static/* {
|
||||||
uri strip_prefix /static
|
uri strip_prefix /static
|
||||||
file_server {
|
file_server {
|
||||||
|
@ -17,5 +17,5 @@ http://127.0.0.1:8002 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reverse_proxy 127.0.0.1:5842
|
reverse_proxy http://frontend:5842
|
||||||
}
|
}
|
||||||
|
|
996
Cargo.lock
generated
996
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,2 +1,2 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["asklyphe-common", "asklyphe-frontend", "searchservice", "asklyphe-auth-frontend", "unit_converter", "authservice", "authservice/migration", "authservice/entity", "bingservice", "googleservice"]
|
members = ["asklyphe-common", "asklyphe-frontend", "asklyphe-auth-frontend", "unit_converter", "authservice", "authservice/migration", "authservice/entity", "bingservice", "googleservice", "vorebot", "lyphedb", "lyphedb/ldbtesttool"]
|
||||||
|
|
19
Dockerfile.auth-frontend
Normal file
19
Dockerfile.auth-frontend
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
FROM rust:1.89.0 AS builder
|
||||||
|
|
||||||
|
WORKDIR /usr/src/asklyphe/
|
||||||
|
COPY asklyphe-auth-frontend asklyphe-auth-frontend
|
||||||
|
COPY asklyphe-common asklyphe-common
|
||||||
|
COPY lyphedb lyphedb
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=$CARGO_HOME/registry,sharing=locked \
|
||||||
|
--mount=type=cache,target=/usr/src/asklyphe/asklyphe-auth-frontend/target \
|
||||||
|
--mount=type=cache,target=$CARGO_HOME/git/db,sharing=locked \
|
||||||
|
cargo install --debug --path asklyphe-auth-frontend/
|
||||||
|
|
||||||
|
FROM debian:trixie-slim
|
||||||
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y libssl3 && rm -rf /var/lib/apt-get/lists/*
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/asklyphe-auth-frontend /usr/local/bin/
|
||||||
|
COPY --from=builder /usr/src/asklyphe/asklyphe-auth-frontend/static /data/static
|
||||||
|
VOLUME /data
|
||||||
|
|
||||||
|
CMD ["asklyphe-auth-frontend"]
|
24
Dockerfile.authservice
Normal file
24
Dockerfile.authservice
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
FROM rust:1.89.0 AS builder
|
||||||
|
|
||||||
|
WORKDIR /usr/src/asklyphe/
|
||||||
|
COPY authservice authservice
|
||||||
|
COPY asklyphe-common asklyphe-common
|
||||||
|
COPY lyphedb lyphedb
|
||||||
|
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=$CARGO_HOME/registry,sharing=locked \
|
||||||
|
--mount=type=cache,target=/usr/src/asklyphe/authservice/target \
|
||||||
|
--mount=type=cache,target=$CARGO_HOME/git/db,sharing=locked \
|
||||||
|
cargo install --debug --path authservice/
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=$CARGO_HOME/registry,sharing=locked \
|
||||||
|
--mount=type=cache,target=/usr/src/asklyphe/authservice/migration/target \
|
||||||
|
--mount=type=cache,target=$CARGO_HOME/git/db,sharing=locked \
|
||||||
|
cargo install --debug --path authservice/migration/
|
||||||
|
|
||||||
|
FROM debian:trixie-slim
|
||||||
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y libssl3 && rm -rf /var/lib/apt-get/lists/*
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/authservice /usr/local/bin/
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/migration /usr/local/bin/
|
||||||
|
|
||||||
|
CMD ["authservice"]
|
17
Dockerfile.bingservice
Normal file
17
Dockerfile.bingservice
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
FROM rust:1.89.0 AS builder
|
||||||
|
|
||||||
|
WORKDIR /usr/src/asklyphe/
|
||||||
|
COPY bingservice bingservice
|
||||||
|
COPY asklyphe-common asklyphe-common
|
||||||
|
COPY lyphedb lyphedb
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=$CARGO_HOME/registry,sharing=locked \
|
||||||
|
--mount=type=cache,target=/usr/src/asklyphe/bingservice/target \
|
||||||
|
--mount=type=cache,target=$CARGO_HOME/git/db,sharing=locked \
|
||||||
|
cargo install --debug --path bingservice/
|
||||||
|
|
||||||
|
FROM debian:trixie-slim
|
||||||
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y libssl3 && rm -rf /var/lib/apt-get/lists/*
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/bingservice /usr/local/bin/
|
||||||
|
|
||||||
|
CMD ["bingservice"]
|
20
Dockerfile.frontend
Normal file
20
Dockerfile.frontend
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
FROM rust:1.89.0 AS builder
|
||||||
|
|
||||||
|
WORKDIR /usr/src/asklyphe/
|
||||||
|
COPY asklyphe-frontend asklyphe-frontend
|
||||||
|
COPY asklyphe-common asklyphe-common
|
||||||
|
COPY lyphedb lyphedb
|
||||||
|
COPY unit_converter unit_converter
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=$CARGO_HOME/registry,sharing=locked \
|
||||||
|
--mount=type=cache,target=/usr/src/asklyphe/asklyphe-frontend/target \
|
||||||
|
--mount=type=cache,target=$CARGO_HOME/git/db,sharing=locked \
|
||||||
|
cargo install --debug --path asklyphe-frontend/
|
||||||
|
|
||||||
|
FROM debian:trixie-slim
|
||||||
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y libssl3 && rm -rf /var/lib/apt-get/lists/*
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/asklyphe-frontend /usr/local/bin/
|
||||||
|
COPY --from=builder /usr/src/asklyphe/asklyphe-frontend/static /data/static
|
||||||
|
VOLUME /data
|
||||||
|
|
||||||
|
CMD ["asklyphe-frontend"]
|
17
Dockerfile.googleservice
Normal file
17
Dockerfile.googleservice
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
FROM rust:1.89.0 AS builder
|
||||||
|
|
||||||
|
WORKDIR /usr/src/asklyphe/
|
||||||
|
COPY googleservice googleservice
|
||||||
|
COPY asklyphe-common asklyphe-common
|
||||||
|
COPY lyphedb lyphedb
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=$CARGO_HOME/registry,sharing=locked \
|
||||||
|
--mount=type=cache,target=/usr/src/asklyphe/googleservice/target \
|
||||||
|
--mount=type=cache,target=$CARGO_HOME/git/db,sharing=locked \
|
||||||
|
cargo install --debug --path googleservice/
|
||||||
|
|
||||||
|
FROM debian:trixie-slim
|
||||||
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y libssl3 && rm -rf /var/lib/apt-get/lists/*
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/googleservice /usr/local/bin/
|
||||||
|
|
||||||
|
CMD ["googleservice"]
|
17
Dockerfile.vorebot
Normal file
17
Dockerfile.vorebot
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
FROM rust:1.89.0 AS builder
|
||||||
|
|
||||||
|
WORKDIR /usr/src/asklyphe/
|
||||||
|
COPY vorebot vorebot
|
||||||
|
COPY asklyphe-common asklyphe-common
|
||||||
|
COPY lyphedb lyphedb
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=$CARGO_HOME/registry,sharing=locked \
|
||||||
|
--mount=type=cache,target=/usr/src/asklyphe/vorebot/target \
|
||||||
|
--mount=type=cache,target=$CARGO_HOME/git/db,sharing=locked \
|
||||||
|
cargo install --debug --path vorebot/
|
||||||
|
|
||||||
|
FROM debian:trixie-slim
|
||||||
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y libssl3 && rm -rf /var/lib/apt-get/lists/*
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/vorebot /usr/local/bin/
|
||||||
|
|
||||||
|
CMD ["vorebot"]
|
|
@ -17,7 +17,7 @@ mod login;
|
||||||
|
|
||||||
use std::{env, process};
|
use std::{env, process};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::SocketAddr;
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
@ -66,7 +66,7 @@ async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let opts = Opts {
|
let opts = Opts {
|
||||||
bind_addr: env::var("BIND_ADDR").unwrap_or("0.0.0.0:5843".to_string()).parse().expect("Badly formed BIND_ADDR (Needs to be SocketAddr)"),
|
bind_addr: env::var("BIND_ADDR").unwrap_or("0.0.0.0:5843".to_string()).parse().expect("Badly formed BIND_ADDR (Needs to be SocketAddr)"),
|
||||||
nats_addr: env::var("NATS_ADDR").unwrap_or("127.0.0.1:4222".to_string()).parse().expect("Badly formed NATS_ADDR (Needs to be SocketAddr)"),
|
nats_addr: env::var("NATS_ADDR").unwrap_or("127.0.0.1:4222".to_string()).to_socket_addrs().expect("Badly formed NATS_ADDR (Needs to be SocketAddr)").nth(0).expect("Unable to resolve DNS address of NATS_ADDR"),
|
||||||
nats_cert: env::var("NATS_CERT").expect("NATS_CERT needs to be set"),
|
nats_cert: env::var("NATS_CERT").expect("NATS_CERT needs to be set"),
|
||||||
nats_key: env::var("NATS_KEY").expect("NATS_KEY needs to be set"),
|
nats_key: env::var("NATS_KEY").expect("NATS_KEY needs to be set"),
|
||||||
asklyphe_url: env::var("ASKLYPHE_URL").unwrap_or("https://asklyphe.com".to_string()),
|
asklyphe_url: env::var("ASKLYPHE_URL").unwrap_or("https://asklyphe.com".to_string()),
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block title %}Login{% endblock %}
|
{% block title %}Login{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" href="/static/auth.css"/>
|
<link rel="stylesheet" href="/static/auth.css?git={{ git_commit }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
|
@ -11,10 +11,10 @@
|
||||||
<a id="alyphebig" href="https://asklyphe.com/">
|
<a id="alyphebig" href="https://asklyphe.com/">
|
||||||
<div id="lyphebig">
|
<div id="lyphebig">
|
||||||
<div class="bent-surrounding">
|
<div class="bent-surrounding">
|
||||||
<img id="lypheimg" src="/static/img/lyphebent.png" alt="image of lyphe, our mascot!">
|
<img id="lypheimg" src="/static/img/lyphebent.png?git={{ git_commit }}" alt="image of lyphe, our mascot!">
|
||||||
</div>
|
</div>
|
||||||
<div id="lyphetitle">
|
<div id="lyphetitle">
|
||||||
<img src="/static/img/logo.png" alt="ask Lyphe!"/>
|
<img src="/static/img/logo.png?git={{ git_commit }}" alt="ask Lyphe!"/>
|
||||||
<p>the best search engine!</p>
|
<p>the best search engine!</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block title %}Register{% endblock %}
|
{% block title %}Register{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" href="/static/auth.css"/>
|
<link rel="stylesheet" href="/static/auth.css?git={{ git_commit }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
|
@ -11,10 +11,10 @@
|
||||||
<a id="alyphebig" href="https://asklyphe.com/">
|
<a id="alyphebig" href="https://asklyphe.com/">
|
||||||
<div id="lyphebig">
|
<div id="lyphebig">
|
||||||
<div class="bent-surrounding">
|
<div class="bent-surrounding">
|
||||||
<img id="lypheimg" src="/static/img/lyphebent.png" alt="image of lyphe, our mascot!">
|
<img id="lypheimg" src="/static/img/lyphebent.png?git={{ git_commit }}" alt="image of lyphe, our mascot!">
|
||||||
</div>
|
</div>
|
||||||
<div id="lyphetitle">
|
<div id="lyphetitle">
|
||||||
<img src="/static/img/logo.png" alt="ask Lyphe!"/>
|
<img src="/static/img/logo.png?git={{ git_commit }}" alt="ask Lyphe!"/>
|
||||||
<p>the best search engine!</p>
|
<p>the best search engine!</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
title="AskLyphe"
|
title="AskLyphe"
|
||||||
href="/static/osd.xml" />
|
href="/static/osd.xml" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/shell.css" />
|
<link rel="stylesheet" href="/static/shell.css?git={{ git_commit }}" />
|
||||||
|
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block title %}Verify Email{% endblock %}
|
{% block title %}Verify Email{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" href="/static/auth.css"/>
|
<link rel="stylesheet" href="/static/auth.css?git={{ git_commit }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
|
@ -11,10 +11,10 @@
|
||||||
<a id="alyphebig" href="https://asklyphe.com/">
|
<a id="alyphebig" href="https://asklyphe.com/">
|
||||||
<div id="lyphebig">
|
<div id="lyphebig">
|
||||||
<div class="bent-surrounding">
|
<div class="bent-surrounding">
|
||||||
<img id="lypheimg" src="/static/img/lyphebent.png" alt="image of lyphe, our mascot!">
|
<img id="lypheimg" src="/static/img/lyphebent.png?git={{ git_commit }}" alt="image of lyphe, our mascot!">
|
||||||
</div>
|
</div>
|
||||||
<div id="lyphetitle">
|
<div id="lyphetitle">
|
||||||
<img src="/static/img/logo.png" alt="ask Lyphe!"/>
|
<img src="/static/img/logo.png?git={{ git_commit }}" alt="ask Lyphe!"/>
|
||||||
<p>the best search engine!</p>
|
<p>the best search engine!</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,10 +11,16 @@ license-file = "LICENSE"
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
chrono = "0.4.31"
|
chrono = "0.4.31"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
foundationdb = { version = "0.8.0", features = ["embedded-fdb-include"] }
|
lyphedb = { path = "../lyphedb" }
|
||||||
|
foundationdb = { version = "0.8.0", features = ["embedded-fdb-include"], optional = true }
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
rmp-serde = "1.1.2"
|
rmp-serde = "1.1.2"
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
async-nats = "0.38.0"
|
async-nats = "0.38.0"
|
||||||
ulid = "1.1.0"
|
ulid = "1.1.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
percent-encoding = "2.3.1"
|
||||||
|
sha-rs = "0.1.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
112
asklyphe-common/src/ldb/linkrelstore.rs
Normal file
112
asklyphe-common/src/ldb/linkrelstore.rs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
use crate::ldb::{construct_path, hash, DBConn};
|
||||||
|
use log::{error, warn};
|
||||||
|
use lyphedb::{KVList, KeyDirectory, LDBNatsMessage, LypheDBCommand, PropagationStrategy};
|
||||||
|
|
||||||
|
pub const INCOMINGSTORE: &str = "incomingstore";
|
||||||
|
pub const OUTGOINGSTORE: &str = "outgoingstore";
|
||||||
|
|
||||||
|
pub async fn a_linksto_b(db: &DBConn, a: &str, b: &str) -> Result<(), ()> {
|
||||||
|
let key_sets = vec![
|
||||||
|
(
|
||||||
|
construct_path(&[OUTGOINGSTORE, &hash(a)])
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
a.as_bytes().to_vec(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
construct_path(&[OUTGOINGSTORE, &hash(a), &hash(b)])
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
b.as_bytes().to_vec(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
construct_path(&[INCOMINGSTORE, &hash(b)])
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
b.as_bytes().to_vec(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
construct_path(&[INCOMINGSTORE, &hash(b), &hash(a)])
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
a.as_bytes().to_vec(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::SetKeys(
|
||||||
|
KVList { kvs: key_sets },
|
||||||
|
PropagationStrategy::OnRead,
|
||||||
|
));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Success => Ok(()),
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
error!("bad request for a_linksto_b");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => {
|
||||||
|
error!("not found for a_linksto_b");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn what_links_to_this(db: &DBConn, url: &str) -> Result<Vec<String>, ()> {
|
||||||
|
let path = construct_path(&[INCOMINGSTORE, &hash(url)])
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec();
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::GetKeyDirectory(KeyDirectory { key: path }));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Entries(kvs) => Ok(kvs
|
||||||
|
.kvs
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| String::from_utf8_lossy(&v.1).to_string())
|
||||||
|
.collect()),
|
||||||
|
LDBNatsMessage::Success => {
|
||||||
|
warn!("lyphedb responded with \"success\" to what_links_to_this, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
warn!("bad request for what_links_to_this");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => Ok(vec![]),
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn what_links_from_this(db: &DBConn, url: &str) -> Result<Vec<String>, ()> {
|
||||||
|
let path = construct_path(&[OUTGOINGSTORE, &hash(url)])
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec();
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::GetKeyDirectory(KeyDirectory { key: path }));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Entries(kvs) => Ok(kvs
|
||||||
|
.kvs
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| String::from_utf8_lossy(&v.1).to_string())
|
||||||
|
.collect()),
|
||||||
|
LDBNatsMessage::Success => {
|
||||||
|
warn!("lyphedb responded with \"success\" to what_links_from_this, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
warn!("bad request for what_links_from_this");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => Ok(vec![]),
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
asklyphe-common/src/ldb/linkstore.rs
Normal file
63
asklyphe-common/src/ldb/linkstore.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use log::{error, warn};
|
||||||
|
use lyphedb::{KVList, KeyDirectory, LDBNatsMessage, LypheDBCommand, PropagationStrategy};
|
||||||
|
use crate::ldb::{construct_path, hash, DBConn};
|
||||||
|
|
||||||
|
pub const LINKSTORE: &str = "linkstore";
|
||||||
|
|
||||||
|
pub async fn add_url_to_linkwords(db: &DBConn, linkwords: &[&str], url: &str) -> Result<(), ()> {
|
||||||
|
let mut key_sets = Vec::new();
|
||||||
|
for linkword in linkwords {
|
||||||
|
let path = construct_path(&[LINKSTORE, linkword, &hash(url)]).as_bytes().to_vec();
|
||||||
|
key_sets.push((path, url.as_bytes().to_vec()));
|
||||||
|
}
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::SetKeys(KVList {
|
||||||
|
kvs: key_sets,
|
||||||
|
}, PropagationStrategy::OnRead));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Success => {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
error!("bad request for add_url_to_linkwords");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => {
|
||||||
|
error!("not found for add_url_to_linkwords");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_urls_from_linkword(db: &DBConn, keyword: &str) -> Result<Vec<String>, ()> {
|
||||||
|
let path = construct_path(&[LINKSTORE, keyword]).as_bytes().to_vec();
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::GetKeyDirectory(KeyDirectory {
|
||||||
|
key: path,
|
||||||
|
}));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Entries(kvs) => {
|
||||||
|
Ok(kvs.kvs.into_iter().map(|v| String::from_utf8_lossy(&v.1).to_string()).collect())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::Success => {
|
||||||
|
warn!("lyphedb responded with \"success\" to get_urls_from_linkwords, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
warn!("bad request for get_urls_from_linkwords");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
asklyphe-common/src/ldb/metastore.rs
Normal file
63
asklyphe-common/src/ldb/metastore.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use log::{error, warn};
|
||||||
|
use lyphedb::{KVList, KeyDirectory, LDBNatsMessage, LypheDBCommand, PropagationStrategy};
|
||||||
|
use crate::ldb::{construct_path, hash, DBConn};
|
||||||
|
|
||||||
|
pub const METASTORE: &str = "metastore";
|
||||||
|
|
||||||
|
pub async fn add_url_to_metawords(db: &DBConn, metawords: &[&str], url: &str) -> Result<(), ()> {
|
||||||
|
let mut key_sets = Vec::new();
|
||||||
|
for metaword in metawords {
|
||||||
|
let path = construct_path(&[METASTORE, metaword, &hash(url)]).as_bytes().to_vec();
|
||||||
|
key_sets.push((path, url.as_bytes().to_vec()));
|
||||||
|
}
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::SetKeys(KVList {
|
||||||
|
kvs: key_sets,
|
||||||
|
}, PropagationStrategy::OnRead));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Success => {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
error!("bad request for add_url_to_metawords");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => {
|
||||||
|
error!("not found for add_url_to_metawords");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_urls_from_metaword(db: &DBConn, metaword: &str) -> Result<Vec<String>, ()> {
|
||||||
|
let path = construct_path(&[METASTORE, metaword]).as_bytes().to_vec();
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::GetKeyDirectory(KeyDirectory {
|
||||||
|
key: path,
|
||||||
|
}));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Entries(kvs) => {
|
||||||
|
Ok(kvs.kvs.into_iter().map(|v| String::from_utf8_lossy(&v.1).to_string()).collect())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::Success => {
|
||||||
|
warn!("lyphedb responded with \"success\" to get_urls_from_metawords, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
warn!("bad request for get_urls_from_metawords");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
asklyphe-common/src/ldb/mod.rs
Normal file
63
asklyphe-common/src/ldb/mod.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
pub mod wordstore;
|
||||||
|
pub mod sitestore;
|
||||||
|
pub mod linkstore;
|
||||||
|
pub mod metastore;
|
||||||
|
pub mod titlestore;
|
||||||
|
pub mod linkrelstore;
|
||||||
|
|
||||||
|
use std::hash::{DefaultHasher, Hasher};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::AtomicU64;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use log::warn;
|
||||||
|
use lyphedb::LDBNatsMessage;
|
||||||
|
use percent_encoding::{percent_encode, AsciiSet, CONTROLS, NON_ALPHANUMERIC};
|
||||||
|
use sha_rs::{Sha, Sha256};
|
||||||
|
|
||||||
|
static NEXT_REPLY_ID: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
pub const NOT_ALLOWED_ASCII: AsciiSet = CONTROLS.add(b' ').add(b'/').add(b'.').add(b'\\');
|
||||||
|
|
||||||
|
pub fn hash(str: &str) -> String {
|
||||||
|
let hasher = Sha256::new();
|
||||||
|
hasher.digest(str.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn construct_path(path_elements: &[&str]) -> String {
|
||||||
|
let mut buf = String::new();
|
||||||
|
buf.push_str("ASKLYPHE/");
|
||||||
|
for el in path_elements {
|
||||||
|
buf.push_str(&percent_encode(el.as_bytes(), &NOT_ALLOWED_ASCII).to_string());
|
||||||
|
buf.push('/');
|
||||||
|
}
|
||||||
|
buf.pop();
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DBConn {
|
||||||
|
nats: async_nats::Client,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DBConn {
|
||||||
|
pub fn new(nats: async_nats::Client, name: impl ToString) -> DBConn {
|
||||||
|
DBConn { nats, name: name.to_string() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query(&self, message: LDBNatsMessage) -> LDBNatsMessage {
|
||||||
|
let data = rmp_serde::to_vec(&message).unwrap();
|
||||||
|
let replyto = format!("ldb-reply-{}", NEXT_REPLY_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst));
|
||||||
|
let mut subscriber = self.nats.subscribe(replyto.clone()).await.expect("NATS ERROR");
|
||||||
|
self.nats.publish_with_reply(self.name.clone(), replyto, data.into()).await.expect("NATS ERROR");
|
||||||
|
if let Some(reply) = subscriber.next().await {
|
||||||
|
let reply = rmp_serde::from_slice::<LDBNatsMessage>(&reply.payload);
|
||||||
|
if reply.is_err() {
|
||||||
|
warn!("DECODED BAD MESSAGE FROM LYPHEDB: {}", reply.err().unwrap());
|
||||||
|
return LDBNatsMessage::NotFound;
|
||||||
|
}
|
||||||
|
return reply.unwrap();
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound
|
||||||
|
}
|
||||||
|
}
|
178
asklyphe-common/src/ldb/sitestore.rs
Normal file
178
asklyphe-common/src/ldb/sitestore.rs
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
use crate::ldb::{construct_path, hash, DBConn};
|
||||||
|
use log::{error, warn};
|
||||||
|
use lyphedb::{KVList, KeyDirectory, KeyList, LDBNatsMessage, LypheDBCommand, PropagationStrategy};
|
||||||
|
|
||||||
|
pub const SITESTORE: &str = "sitestore";
|
||||||
|
|
||||||
|
pub const TITLE: &str = "title";
|
||||||
|
pub const DESCRIPTION: &str = "desc";
|
||||||
|
pub const KEYWORDS: &str = "keywords";
|
||||||
|
pub const PAGE_TEXT_RANKING: &str = "ptranks";
|
||||||
|
pub const PAGE_TEXT_RAW: &str = "textraw";
|
||||||
|
pub const DAMPING: &str = "damping";
|
||||||
|
|
||||||
|
pub async fn add_website(
|
||||||
|
db: &DBConn,
|
||||||
|
url: &str,
|
||||||
|
title: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
keywords: Option<Vec<String>>,
|
||||||
|
page_text_ranking: &[(String, f64)],
|
||||||
|
page_text_raw: String,
|
||||||
|
damping: f64,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let keyurl = hash(url);
|
||||||
|
let mut kvs = vec![
|
||||||
|
(
|
||||||
|
construct_path(&[SITESTORE, &keyurl]).as_bytes().to_vec(),
|
||||||
|
url.as_bytes().to_vec(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
construct_path(&[SITESTORE, &keyurl, PAGE_TEXT_RANKING])
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
rmp_serde::to_vec(page_text_ranking).unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
construct_path(&[SITESTORE, &keyurl, PAGE_TEXT_RAW])
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
page_text_raw.as_bytes().to_vec(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
construct_path(&[SITESTORE, &keyurl, DAMPING])
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
damping.to_be_bytes().to_vec(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if let Some(title) = title {
|
||||||
|
kvs.push((
|
||||||
|
construct_path(&[SITESTORE, &keyurl, TITLE]).as_bytes().to_vec(),
|
||||||
|
title.as_bytes().to_vec(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
if let Some(description) = description {
|
||||||
|
kvs.push((
|
||||||
|
construct_path(&[SITESTORE, &keyurl, DESCRIPTION])
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
description.as_bytes().to_vec(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
if let Some(keywords) = keywords {
|
||||||
|
kvs.push((
|
||||||
|
construct_path(&[SITESTORE, &keyurl, KEYWORDS])
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
rmp_serde::to_vec(&keywords).unwrap(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::SetKeys(
|
||||||
|
KVList { kvs },
|
||||||
|
PropagationStrategy::OnRead,
|
||||||
|
));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Success => Ok(()),
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
error!("bad request for add_website");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => {
|
||||||
|
error!("not found for add_website");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct WebsiteData {
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub keywords: Option<Vec<String>>,
|
||||||
|
pub page_text_ranking: Vec<(String, f64)>,
|
||||||
|
pub page_text_raw: String,
|
||||||
|
pub damping: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_website(db: &DBConn, url: &str) -> Result<WebsiteData, ()> {
|
||||||
|
let keyurl = hash(url);
|
||||||
|
let keys = [
|
||||||
|
construct_path(&[SITESTORE, &keyurl, TITLE]).as_bytes().to_vec(),
|
||||||
|
construct_path(&[SITESTORE, &keyurl, DESCRIPTION]).as_bytes().to_vec(),
|
||||||
|
construct_path(&[SITESTORE, &keyurl, KEYWORDS]).as_bytes().to_vec(),
|
||||||
|
construct_path(&[SITESTORE, &keyurl, PAGE_TEXT_RANKING]).as_bytes().to_vec(),
|
||||||
|
construct_path(&[SITESTORE, &keyurl, PAGE_TEXT_RAW]).as_bytes().to_vec(),
|
||||||
|
construct_path(&[SITESTORE, &keyurl, DAMPING]).as_bytes().to_vec(),
|
||||||
|
].to_vec();
|
||||||
|
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::GetKeys(KeyList { keys }));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Entries(kvlist) => {
|
||||||
|
let mut data = WebsiteData::default();
|
||||||
|
for (key, value) in kvlist.kvs {
|
||||||
|
let key = String::from_utf8_lossy(&key).to_string();
|
||||||
|
match key.as_str() {
|
||||||
|
_ if key.ends_with(TITLE) => {
|
||||||
|
data.title = Some(String::from_utf8_lossy(&value).to_string());
|
||||||
|
}
|
||||||
|
_ if key.ends_with(DESCRIPTION) => {
|
||||||
|
data.description = Some(String::from_utf8_lossy(&value).to_string());
|
||||||
|
}
|
||||||
|
_ if key.ends_with(KEYWORDS) => {
|
||||||
|
let deser = rmp_serde::from_slice::<Vec<String>>(&value);
|
||||||
|
if let Err(e) = deser {
|
||||||
|
error!("bad keywords entry for {}, deser error: {:?}", key, e);
|
||||||
|
} else {
|
||||||
|
data.keywords = Some(deser.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ if key.ends_with(PAGE_TEXT_RANKING) => {
|
||||||
|
let deser = rmp_serde::from_slice::<Vec<(String, f64)>>(&value);
|
||||||
|
if let Err(e) = deser {
|
||||||
|
error!("bad page_text_ranking entry for {}, deser error: {:?}", key, e);
|
||||||
|
} else {
|
||||||
|
data.page_text_ranking = deser.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ if key.ends_with(PAGE_TEXT_RAW) => {
|
||||||
|
data.page_text_raw = String::from_utf8_lossy(&value).to_string();
|
||||||
|
}
|
||||||
|
_ if key.ends_with(DAMPING) => {
|
||||||
|
data.damping = f64::from_be_bytes(value.try_into().unwrap_or(0.85f64.to_be_bytes()));
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
warn!("encountered weird returned key for get_website");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
LDBNatsMessage::Success => {
|
||||||
|
warn!("lyphedb responded with \"success\" to get_website, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
error!("bad request for get_website");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => {
|
||||||
|
warn!("not found for get_website");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
asklyphe-common/src/ldb/titlestore.rs
Normal file
63
asklyphe-common/src/ldb/titlestore.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use log::{error, warn};
|
||||||
|
use lyphedb::{KVList, KeyDirectory, LDBNatsMessage, LypheDBCommand, PropagationStrategy};
|
||||||
|
use crate::ldb::{construct_path, hash, DBConn};
|
||||||
|
|
||||||
|
pub const TITLESTORE: &str = "titlestore";
|
||||||
|
|
||||||
|
pub async fn add_url_to_titlewords(db: &DBConn, titlewords: &[&str], url: &str) -> Result<(), ()> {
|
||||||
|
let mut key_sets = Vec::new();
|
||||||
|
for titleword in titlewords {
|
||||||
|
let path = construct_path(&[TITLESTORE, titleword, &hash(url)]).as_bytes().to_vec();
|
||||||
|
key_sets.push((path, url.as_bytes().to_vec()));
|
||||||
|
}
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::SetKeys(KVList {
|
||||||
|
kvs: key_sets,
|
||||||
|
}, PropagationStrategy::OnRead));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Success => {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
error!("bad request for add_url_to_titlewords");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => {
|
||||||
|
error!("not found for add_url_to_titlewords");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_urls_from_titleword(db: &DBConn, titleword: &str) -> Result<Vec<String>, ()> {
|
||||||
|
let path = construct_path(&[TITLESTORE, titleword]).as_bytes().to_vec();
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::GetKeyDirectory(KeyDirectory {
|
||||||
|
key: path,
|
||||||
|
}));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Entries(kvs) => {
|
||||||
|
Ok(kvs.kvs.into_iter().map(|v| String::from_utf8_lossy(&v.1).to_string()).collect())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::Success => {
|
||||||
|
warn!("lyphedb responded with \"success\" to get_urls_from_titlewords, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
warn!("bad request for get_urls_from_titlewords");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
asklyphe-common/src/ldb/wordstore.rs
Normal file
64
asklyphe-common/src/ldb/wordstore.rs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
use log::{error, warn};
|
||||||
|
use lyphedb::{KVList, KeyDirectory, LDBNatsMessage, LypheDBCommand, PropagationStrategy};
|
||||||
|
use crate::ldb::{construct_path, hash, DBConn};
|
||||||
|
|
||||||
|
pub const WORDSTORE: &str = "wordstore";
|
||||||
|
|
||||||
|
pub async fn add_url_to_keywords(db: &DBConn, keywords: &[&str], url: &str) -> Result<(), ()> {
|
||||||
|
let mut key_sets = Vec::new();
|
||||||
|
for keyword in keywords {
|
||||||
|
let path = construct_path(&[WORDSTORE, keyword, &hash(url)]).as_bytes().to_vec();
|
||||||
|
let data = url.as_bytes().to_vec();
|
||||||
|
key_sets.push((path, data));
|
||||||
|
}
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::SetKeys(KVList {
|
||||||
|
kvs: key_sets,
|
||||||
|
}, PropagationStrategy::OnRead));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Success => {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
error!("bad request for add_url_to_keywords");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => {
|
||||||
|
error!("not found for add_url_to_keywords");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_urls_from_keyword(db: &DBConn, keyword: &str) -> Result<Vec<String>, ()> {
|
||||||
|
let path = construct_path(&[WORDSTORE, keyword]).as_bytes().to_vec();
|
||||||
|
let cmd = LDBNatsMessage::Command(LypheDBCommand::GetKeyDirectory(KeyDirectory {
|
||||||
|
key: path,
|
||||||
|
}));
|
||||||
|
|
||||||
|
match db.query(cmd).await {
|
||||||
|
LDBNatsMessage::Entries(kvs) => {
|
||||||
|
Ok(kvs.kvs.into_iter().map(|v| String::from_utf8_lossy(&v.1).to_string()).collect())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::Success => {
|
||||||
|
warn!("lyphedb responded with \"success\" to get_urls_from_keywords, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::BadRequest => {
|
||||||
|
warn!("bad request for get_urls_from_keywords");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
LDBNatsMessage::NotFound => {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("lyphedb sent weird message as response, treating as error");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,8 +12,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pub mod nats;
|
pub mod nats;
|
||||||
|
#[cfg(feature = "foundationdb")]
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
pub mod ldb;
|
||||||
|
|
||||||
|
pub use lyphedb;
|
||||||
|
#[cfg(feature = "foundationdb")]
|
||||||
pub use foundationdb;
|
pub use foundationdb;
|
||||||
|
|
||||||
pub fn add(left: usize, right: usize) -> usize {
|
pub fn add(left: usize, right: usize) -> usize {
|
||||||
|
|
|
@ -18,7 +18,7 @@ pub const VOREBOT_NEWHOSTNAME_SERVICE: &str = "websiteparse_highpriority";
|
||||||
pub const VOREBOT_SUGGESTED_SERVICE: &str = "websiteparse_highestpriority";
|
pub const VOREBOT_SUGGESTED_SERVICE: &str = "websiteparse_highestpriority";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct WebParseRequest {
|
pub struct CrawlRequest {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub damping_factor: f32,
|
pub damping: f64,
|
||||||
}
|
}
|
|
@ -36,5 +36,9 @@ once_cell = "1.19.0"
|
||||||
chrono = "0.4.33"
|
chrono = "0.4.33"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
url_encoded_data = "0.6.1"
|
url_encoded_data = "0.6.1"
|
||||||
|
strum = "0.27.1"
|
||||||
|
strum_macros = "0.27.1"
|
||||||
|
|
||||||
|
astro-float = "0.9.2"
|
||||||
|
|
||||||
env_logger = "*"
|
env_logger = "*"
|
||||||
|
|
106
asklyphe-frontend/src/bangs.rs
Normal file
106
asklyphe-frontend/src/bangs.rs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
use tracing::{debug, error};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use url_encoded_data;
|
||||||
|
|
||||||
|
pub static BANG_PREFIX: &str = "!";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Bang<'a> {
|
||||||
|
pub url: &'a str,
|
||||||
|
pub aliases: &'a [&'a str]
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Bang<'_> {
|
||||||
|
fn new(url: &'a str, aliases: &'a [&'a str]) -> Bang<'a> {
|
||||||
|
Bang {url, aliases}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static BUILTIN_BANGS: Lazy<BTreeMap<&str, Bang>> = Lazy::new(|| {
|
||||||
|
let mut bangs = BTreeMap::new();
|
||||||
|
bangs.insert("Google", Bang::new("https://google.com/search?q={}", &["g", "google"] as &[&str]));
|
||||||
|
|
||||||
|
bangs.insert("DuckDuckGo", Bang::new("https://duckduckgo.com/?q={}", &["d", "ddg", "duckduckgo"] as &[&str]));
|
||||||
|
|
||||||
|
bangs.insert("Wikipedia", Bang::new("https://wikipedia.org/w/index.php?search={}", &["w", "wiki", "wikipedia"] as &[&str]));
|
||||||
|
bangs
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct BangLoc<'b> {
|
||||||
|
pub url: &'b str,
|
||||||
|
pub start_idx: usize,
|
||||||
|
pub len: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'b> BangLoc<'_> {
|
||||||
|
fn new(url: &'b str, start_idx: usize, len: usize) -> BangLoc<'b> {
|
||||||
|
BangLoc {url, start_idx, len}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn redirect_bang(query: &String) -> Option<String> {
|
||||||
|
if !query.contains(BANG_PREFIX) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let bangs = query.match_indices(BANG_PREFIX).filter(|(bang_start_idx, _)| {
|
||||||
|
if *bang_start_idx == 0 || query.chars().nth(*bang_start_idx - 1).unwrap().is_whitespace() {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}).map(|(bang_start_idx, _)| {
|
||||||
|
let rest = query.get(bang_start_idx + 1..query.len()).unwrap();
|
||||||
|
BUILTIN_BANGS.iter().map(|(_, bang)| {
|
||||||
|
let alias = bang.aliases.iter()
|
||||||
|
.filter(|alias| rest.starts_with(**alias))
|
||||||
|
.filter(
|
||||||
|
|alias| rest.chars()
|
||||||
|
.nth(alias.len())
|
||||||
|
.unwrap_or(' ')
|
||||||
|
.is_whitespace())
|
||||||
|
.max_by(|a, b| a.len().cmp(&b.len()))?;
|
||||||
|
Some(BangLoc::new(bang.url, bang_start_idx, alias.len()))
|
||||||
|
}).filter(|bang| bang.is_some()).map(|bang| bang.unwrap()).next()
|
||||||
|
}).filter(|bang| bang.is_some())
|
||||||
|
.map(|bang| bang.unwrap())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
|
||||||
|
let bang = bangs.first()?;
|
||||||
|
let end_idx = {
|
||||||
|
let mut end_idx = bang.start_idx + 1 + bang.len;
|
||||||
|
if end_idx < query.len() {
|
||||||
|
end_idx += 1;
|
||||||
|
}
|
||||||
|
end_idx
|
||||||
|
};
|
||||||
|
|
||||||
|
let start_idx = if end_idx == query.len() && bang.start_idx > 0 {
|
||||||
|
bang.start_idx - 1
|
||||||
|
} else {
|
||||||
|
bang.start_idx
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let query_split = query.split_once(query.get(start_idx..end_idx).unwrap()).unwrap();
|
||||||
|
|
||||||
|
let query_trimmed = format!("{}{}", query_split.0, query_split.1);
|
||||||
|
|
||||||
|
// A hack to get URL escaping without using a proper URL layout, hopefully has no other issues apart from prepending '=' to the string
|
||||||
|
let query_encoded = url_encoded_data::stringify(&[("", query_trimmed.as_str())]);
|
||||||
|
let query_encoded = query_encoded.get(1..query_encoded.len()).unwrap().to_owned();
|
||||||
|
|
||||||
|
|
||||||
|
let bang_url_split = bang.url.split_once("{}").unwrap();
|
||||||
|
|
||||||
|
let bang_url = format!(
|
||||||
|
"{}{}{}",
|
||||||
|
bang_url_split.0,
|
||||||
|
query_encoded,
|
||||||
|
bang_url_split.1
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(bang_url)
|
||||||
|
}
|
|
@ -14,11 +14,13 @@
|
||||||
pub mod searchbot;
|
pub mod searchbot;
|
||||||
pub mod wikipedia;
|
pub mod wikipedia;
|
||||||
pub mod unit_converter;
|
pub mod unit_converter;
|
||||||
|
pub mod bangs;
|
||||||
|
pub mod math;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
|
||||||
use std::{env, process};
|
use std::{env, process};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::SocketAddr;
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
@ -84,7 +86,7 @@ async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let opts = Opts {
|
let opts = Opts {
|
||||||
bind_addr: env::var("BIND_ADDR").unwrap_or("0.0.0.0:5842".to_string()).parse().expect("Badly formed BIND_ADDR (Needs to be SocketAddr)"),
|
bind_addr: env::var("BIND_ADDR").unwrap_or("0.0.0.0:5842".to_string()).parse().expect("Badly formed BIND_ADDR (Needs to be SocketAddr)"),
|
||||||
nats_addr: env::var("NATS_ADDR").unwrap_or("127.0.0.1:4222".to_string()).parse().expect("Badly formed NATS_ADDR (Needs to be SocketAddr)"),
|
nats_addr: env::var("NATS_ADDR").unwrap_or("127.0.0.1:4222".to_string()).to_socket_addrs().expect("Badly formed NATS_ADDR (Needs to be SocketAddr)").nth(0).expect("Unable to resolve DNS address of NATS_ADDR"),
|
||||||
nats_cert: env::var("NATS_CERT").expect("NATS_CERT needs to be set"),
|
nats_cert: env::var("NATS_CERT").expect("NATS_CERT needs to be set"),
|
||||||
nats_key: env::var("NATS_KEY").expect("NATS_KEY needs to be set"),
|
nats_key: env::var("NATS_KEY").expect("NATS_KEY needs to be set"),
|
||||||
auth_url: env::var("AUTH_URL").unwrap_or("https://auth.asklyphe.com".to_string()),
|
auth_url: env::var("AUTH_URL").unwrap_or("https://auth.asklyphe.com".to_string()),
|
||||||
|
|
456
asklyphe-frontend/src/math.rs
Normal file
456
asklyphe-frontend/src/math.rs
Normal file
|
@ -0,0 +1,456 @@
|
||||||
|
use tracing::{debug, error};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use astro_float::{BigFloat, Sign, RoundingMode, Consts};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
pub const PRECISION: usize = 2048;
|
||||||
|
static CONST_CACHE: Lazy<Arc<Mutex<Consts>>> = Lazy::new(|| Arc::new(Mutex::new(Consts::new().expect("Unable to allocate memory for Conts cache"))));
|
||||||
|
// static PI: Lazy<BigFloat> = Lazy::new(|| BigFloat::from_str("3.141592653589793238462643383279").unwrap());
|
||||||
|
// static E: Lazy<BigFloat> = Lazy::new(|| BigFloat::from_str("2.718281828459045235360287471352").unwrap());
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Calculation {
|
||||||
|
pub equation: String,
|
||||||
|
pub result: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle partial match in query where <words> (valid equation) <words> gets parsed
|
||||||
|
// TODO: have some option to switch between degrees and radians in the settings/search query params (or maybe have a function that converts between them instead?)
|
||||||
|
pub fn calculate(query: &str) -> Option<Calculation> {
|
||||||
|
debug!("Got query {}", query);
|
||||||
|
let mut parser = Parser::new(Lexer::new(query));
|
||||||
|
let mut tree = parser.parse()?;
|
||||||
|
let res = tree.eval();
|
||||||
|
let res_float = f64::from_str(&format!("{}", res)).unwrap();
|
||||||
|
|
||||||
|
debug!("Calculation: {}", query);
|
||||||
|
debug!("Tree: {:?}", tree);
|
||||||
|
debug!("Result: {:?}", res_float);
|
||||||
|
Some(Calculation {equation: query.to_string(), result: res_float.to_string()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: put into own crate with dependency astro-float = "0.9.2" so I can use more than BigFloat
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
enum Token {
|
||||||
|
Op(Op),
|
||||||
|
Atom(Atom),
|
||||||
|
/* Number(BigFloat),
|
||||||
|
Func(Func),*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
enum Op {
|
||||||
|
Add,
|
||||||
|
Subtract,
|
||||||
|
Multiply,
|
||||||
|
Divide,
|
||||||
|
Exponent,
|
||||||
|
LParen,
|
||||||
|
RParen,
|
||||||
|
Func(Func), // A function is an Op that takes whatever the next thing is and binds it, either the next number or whatever is in parens
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Op {
|
||||||
|
fn bp_infix(&self) -> Option<(f64, f64)> {
|
||||||
|
match self {
|
||||||
|
// Op::LParen => Some(0.0),
|
||||||
|
// Op::RParen => Some(0.0),
|
||||||
|
Op::Add => Some((1.0, 1.1)),
|
||||||
|
Op::Subtract => Some((1.0, 1.1)),
|
||||||
|
Op::Multiply => Some((2.0, 2.1)),
|
||||||
|
Op::Divide => Some((2.0, 2.1)),
|
||||||
|
Op::Exponent => Some((3.1, 3.0)),
|
||||||
|
_ => None,
|
||||||
|
// Op::Func(_) => 0.0, // TODO: decide if this is a good LBP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bp_prefix(&self) -> Option<f64> {
|
||||||
|
match self {
|
||||||
|
Op::Func(_) => Some(6.0),
|
||||||
|
Op::Subtract => Some(5.0),
|
||||||
|
Op::Add => Some(5.0),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_to(&self, args: &mut Vec<Expr>) -> BigFloat {
|
||||||
|
match args.len() {
|
||||||
|
1 => match self {
|
||||||
|
Op::Subtract => {
|
||||||
|
let mut res = args[0].eval();
|
||||||
|
res.set_sign(Sign::Neg);
|
||||||
|
res
|
||||||
|
},
|
||||||
|
Op::Add => {
|
||||||
|
let mut res = args[0].eval();
|
||||||
|
res.set_sign(Sign::Pos);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
Op::Func(f) => match f {
|
||||||
|
Func::Sine => args[0].eval().sin(PRECISION, RoundingMode::None, &mut CONST_CACHE.lock().unwrap()),
|
||||||
|
Func::Cosine => args[0].eval().cos(PRECISION, RoundingMode::None, &mut CONST_CACHE.lock().unwrap()),
|
||||||
|
Func::Tangent => args[0].eval().tan(PRECISION, RoundingMode::None, &mut CONST_CACHE.lock().unwrap()),
|
||||||
|
Func::ArcSine => args[0].eval().asin(PRECISION, RoundingMode::None, &mut CONST_CACHE.lock().unwrap()),
|
||||||
|
Func::ArcCosine => args[0].eval().acos(PRECISION, RoundingMode::None, &mut CONST_CACHE.lock().unwrap()),
|
||||||
|
Func::ArcTangent => args[0].eval().atan(PRECISION, RoundingMode::None, &mut CONST_CACHE.lock().unwrap()),
|
||||||
|
Func::Log2 => args[0].eval().log2(PRECISION, RoundingMode::None, &mut CONST_CACHE.lock().unwrap()),
|
||||||
|
Func::Log10 => args[0].eval().log10(PRECISION, RoundingMode::None, &mut CONST_CACHE.lock().unwrap()),
|
||||||
|
Func::LogN => args[0].eval().ln(PRECISION, RoundingMode::None, &mut CONST_CACHE.lock().unwrap()),
|
||||||
|
Func::Square => args[0].eval().pow(&BigFloat::from_f64(2.0, PRECISION), PRECISION, RoundingMode::None, &mut CONST_CACHE.lock().unwrap()),
|
||||||
|
Func::SquareRoot => args[0].eval().sqrt(PRECISION, RoundingMode::None),
|
||||||
|
Func::Abs => args[0].eval().abs(),
|
||||||
|
Func::Deg => args[0].eval().mul(&Const::Pi.get_val().div(&BigFloat::from_f64(180.0, PRECISION), PRECISION, RoundingMode::None), PRECISION, RoundingMode::None),
|
||||||
|
Func::Rad => args[0].eval().mul(&BigFloat::from_f64(180.0, PRECISION).div(&Const::Pi.get_val(), PRECISION, RoundingMode::None), PRECISION, RoundingMode::None),
|
||||||
|
_ => {
|
||||||
|
error!("Got 1 params for func {:?} which expects 2 (should not be possible)", self);
|
||||||
|
astro_float::NAN
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
error!("Got 1 params for {:?} which expects 2 (should not be possible)", self);
|
||||||
|
astro_float::NAN
|
||||||
|
},
|
||||||
|
}
|
||||||
|
2 => match self {
|
||||||
|
Op::LParen => args[0].eval(),
|
||||||
|
Op::RParen => args[0].eval(),
|
||||||
|
Op::Add => args[0].eval().add(&mut args[1].eval(), PRECISION, RoundingMode::None),
|
||||||
|
Op::Subtract => args[0].eval().sub(&mut args[1].eval(), PRECISION, RoundingMode::None),
|
||||||
|
Op::Multiply => args[0].eval().mul(&mut args[1].eval(), PRECISION, RoundingMode::None),
|
||||||
|
Op::Divide => args[0].eval().div(&mut args[1].eval(), PRECISION, RoundingMode::None),
|
||||||
|
Op::Exponent => args[0].eval().pow(&mut args[1].eval(), PRECISION, RoundingMode::None, &mut CONST_CACHE.lock().unwrap()),
|
||||||
|
Op::Func(Func::Log) => args[0].eval().log(&mut args[1].eval(), PRECISION, RoundingMode::None, &mut CONST_CACHE.lock().unwrap()),
|
||||||
|
_ => {
|
||||||
|
error!("Got 2 params for {:?} which only expects 1 (should not be possible)", self);
|
||||||
|
astro_float::NAN
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("Unexpected number of params ({}) for {:?} (should not be possible)", args.len(), self);
|
||||||
|
astro_float::NAN
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
enum Atom {
|
||||||
|
Number(BigFloat),
|
||||||
|
Const(Const),
|
||||||
|
}
|
||||||
|
|
||||||
|
/*impl Atom {
|
||||||
|
fn get_val(&self) -> BigFloat {
|
||||||
|
match self {
|
||||||
|
Atom::Number(val) => *val,
|
||||||
|
Atom::Const(c) => match c {
|
||||||
|
Const::Pi => CONST_CACHE.lock().unwrap().pi(PRECISION, RoundingMode::None),
|
||||||
|
Const::E => CONST_CACHE.lock().unwrap().e(PRECISION, RoundingMode::None),
|
||||||
|
Const::Inf => astro_float::INF_POS,
|
||||||
|
Const::Nan => astro_float::NAN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
impl Const {
|
||||||
|
fn get_val(&self) -> BigFloat {
|
||||||
|
match self {
|
||||||
|
Const::Pi => CONST_CACHE.lock().unwrap().pi(PRECISION, RoundingMode::None),
|
||||||
|
Const::E => CONST_CACHE.lock().unwrap().e(PRECISION, RoundingMode::None),
|
||||||
|
Const::Inf => astro_float::INF_POS,
|
||||||
|
Const::Nan => astro_float::NAN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
enum Func {
|
||||||
|
Sine,
|
||||||
|
Cosine,
|
||||||
|
Tangent,
|
||||||
|
// sin-1, cos-1, tan-1
|
||||||
|
ArcSine,
|
||||||
|
ArcCosine,
|
||||||
|
ArcTangent,
|
||||||
|
Log2,
|
||||||
|
Log10,
|
||||||
|
LogN,
|
||||||
|
Log,
|
||||||
|
Square,
|
||||||
|
SquareRoot,
|
||||||
|
Abs,
|
||||||
|
Deg,
|
||||||
|
Rad,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Func {
|
||||||
|
fn names() -> &'static [(Func, &'static [&'static str])] {
|
||||||
|
&[
|
||||||
|
(Func::Sine, &["sin", "sine"]),
|
||||||
|
(Func::Cosine, &["cos", "cosine"]),
|
||||||
|
(Func::Tangent, &["tan", "tangent"]),
|
||||||
|
(Func::ArcSine, &["asin", "asine", "arcsin", "arcsine"]),
|
||||||
|
(Func::ArcCosine, &["acos", "acosine", "arccos", "arccosine"]),
|
||||||
|
(Func::ArcTangent, &["atan", "atangent", "arctan", "arctangent"]),
|
||||||
|
(Func::Log2, &["log2"]),
|
||||||
|
(Func::Log10, &["log10"]),
|
||||||
|
(Func::LogN, &["ln", "logn"]),
|
||||||
|
(Func::Log, &["log"]),
|
||||||
|
(Func::Square, &["square", "squared"]),
|
||||||
|
(Func::SquareRoot, &["sqrt", "squareroot", "√"]),
|
||||||
|
(Func::Abs, &["abs", "absolute"]),
|
||||||
|
(Func::Deg, &["deg", "degrees", "deg2rad"]),
|
||||||
|
(Func::Rad, &["rad", "radians", "rad2deg"]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
enum Const {
|
||||||
|
Pi,
|
||||||
|
E,
|
||||||
|
Inf,
|
||||||
|
Nan,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Const {
|
||||||
|
fn names() -> &'static [(Const, &'static [&'static str])] {
|
||||||
|
&[
|
||||||
|
(Const::Pi, &["pi", "PI", "π"]),
|
||||||
|
(Const::E, &["e", "euler"]),
|
||||||
|
(Const::Inf, &["inf", "infinity", "∞"]),
|
||||||
|
(Const::Nan, &["nan", "NaN"])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
enum ParseErr {
|
||||||
|
Eof,
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
// this can probably be swapped out with a lexer generator like Logos if needed
|
||||||
|
|
||||||
|
struct Lexer<'a> {
|
||||||
|
data: &'a str,
|
||||||
|
data_ptr: &'a str,
|
||||||
|
// idx: usize,
|
||||||
|
next_by: usize,
|
||||||
|
next_tok: Result<Token, ParseErr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: refactor with iterator that returns Option(Token) where one token option is Eof (or a enum of Token(Token) and Eof, or just Option(Option(Token)))
|
||||||
|
impl Lexer<'_> {
|
||||||
|
|
||||||
|
fn new(data: &str) -> Lexer<'_> {
|
||||||
|
let mut n: Lexer = Lexer {data, data_ptr: data, next_by: 0, next_tok: Err(ParseErr::Eof)};
|
||||||
|
n.next();
|
||||||
|
debug!("New finished!");
|
||||||
|
n
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _next(&mut self) -> Result<Token, ParseErr> {
|
||||||
|
self.data_ptr = &self.data_ptr[self.next_by..];
|
||||||
|
match self.data_ptr.chars().nth(0) {
|
||||||
|
Some(val) => {
|
||||||
|
debug!("lexing char '{}' at idx {}", val, self.data.chars().count() - self.data_ptr.chars().count());
|
||||||
|
self.next_by = 1;
|
||||||
|
match val {
|
||||||
|
'+' => Ok(Token::Op(Op::Add)),
|
||||||
|
'-' => Ok(Token::Op(Op::Subtract)),
|
||||||
|
'×' | '*' => Ok(Token::Op(Op::Multiply)),
|
||||||
|
'÷' | '/' => Ok(Token::Op(Op::Divide)),
|
||||||
|
'^' => Ok(Token::Op(Op::Exponent)),
|
||||||
|
'(' => Ok(Token::Op(Op::LParen)),
|
||||||
|
')' => Ok(Token::Op(Op::RParen)),
|
||||||
|
_ if val.is_whitespace() => self._next(),
|
||||||
|
_ if val.is_digit(10) => {
|
||||||
|
let mut len: usize = 0;
|
||||||
|
self.data_ptr.chars().take_while(|c| c.is_digit(10) || *c == '.').for_each(|_| len += 1);
|
||||||
|
self.next_by = len;
|
||||||
|
|
||||||
|
match self.data_ptr[..len].parse() {
|
||||||
|
Ok(val) => Ok(Token::Atom(Atom::Number(val))),
|
||||||
|
Err(e) => Err(ParseErr::Invalid),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
let len = self.data_ptr.chars().count();
|
||||||
|
for (f, names) in Func::names() {
|
||||||
|
for name in *names {
|
||||||
|
let n_len = name.chars().count();
|
||||||
|
if self.data_ptr.starts_with(name) && (len == n_len || !self.data_ptr.chars().nth(n_len).unwrap().is_alphanumeric()) {
|
||||||
|
self.next_by = n_len;
|
||||||
|
return Ok(Token::Op(Op::Func(*f)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (f, names) in Const::names() {
|
||||||
|
for name in *names {
|
||||||
|
let n_len = name.chars().count();
|
||||||
|
if self.data_ptr.starts_with(name) && (len == n_len || !self.data_ptr.chars().nth(n_len).unwrap().is_alphanumeric()) {
|
||||||
|
self.next_by = n_len;
|
||||||
|
return Ok(Token::Atom(Atom::Const(*f)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("got invalid char '{}'", val);
|
||||||
|
Err(ParseErr::Invalid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.next_by = 0;
|
||||||
|
Err(ParseErr::Eof)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&mut self) -> Result<Token, ParseErr> {
|
||||||
|
let res = self._next();
|
||||||
|
let val = mem::replace(&mut self.next_tok, res);
|
||||||
|
// self.next_tok = self._next();
|
||||||
|
val
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek(&mut self) -> &Result<Token, ParseErr> {
|
||||||
|
&self.next_tok
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with iterator so I can do parser.parse(lexer.iter()) and parse does lex_iter.next() & such
|
||||||
|
fn lex_all(&mut self) -> Option<Vec<Token>> {
|
||||||
|
let mut tokens: Vec<Token> = vec![];
|
||||||
|
loop {
|
||||||
|
match self.next() {
|
||||||
|
Err(ParseErr::Eof) => return Some(tokens),
|
||||||
|
Err(ParseErr::Invalid) => return None,
|
||||||
|
Ok(tok) => tokens.push(tok),
|
||||||
|
}
|
||||||
|
// debug!("tokens: {:?}", tokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches(s: &str, check: &str) -> usize {
|
||||||
|
// debug!("s: \"{}\", check: \"{}\"c_len: {}, s_len: {}, s[c_len]: {:?}, s[c_len + 1]: {:?}", s, check, check.chars().count(), s.chars().count(), s.chars().nth(check.chars().count()), s.chars().nth(check.chars().count() + 1));
|
||||||
|
match (s.chars().count(), check.chars().count()) {
|
||||||
|
(s_len, c_len) if s_len < c_len => 0,
|
||||||
|
(s_len, c_len) if s_len == c_len && s == check => c_len - 1,
|
||||||
|
(s_len, c_len) if s_len > c_len && s.starts_with(check) && s.chars().nth(c_len).unwrap().is_whitespace() => c_len,
|
||||||
|
(_, _) => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Parser<'a> {
|
||||||
|
lex: Lexer<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser<'_> {
|
||||||
|
fn new(lex: Lexer) -> Parser { Parser {lex} }
|
||||||
|
|
||||||
|
fn parse(&mut self) -> Option<Expr> {
|
||||||
|
self.parse_expr(0.0).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_expr(&mut self, min_bp: f64) -> Result<Expr, ParseErr> {
|
||||||
|
/*while let Ok(val) = self.lex.next() {debug!("token: {:?}", val)}
|
||||||
|
match self.lex.next().err() {
|
||||||
|
|
||||||
|
_ => return Err(ParseErr::Invalid),
|
||||||
|
}*/
|
||||||
|
let mut lhs: Expr = match self.lex.next() {
|
||||||
|
Ok(val) => match val {
|
||||||
|
Token::Atom(val) => Ok(Expr::Atom(val)),
|
||||||
|
Token::Op(op) => match op {
|
||||||
|
Op::LParen => {
|
||||||
|
let val = self.parse_expr(0.0);
|
||||||
|
if self.lex.next() != Ok(Token::Op(Op::RParen)) {
|
||||||
|
debug!("Unclosed parens");
|
||||||
|
Err(ParseErr::Invalid)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Op::Func(f) => Ok(Expr::Node(Op::Func(f), vec![self.parse_expr(op.get_lbp())?])),
|
||||||
|
_ => match op.bp_prefix() {
|
||||||
|
Some(bp) => Ok(Expr::Node(op, vec![self.parse_expr(bp)?])),
|
||||||
|
None => {debug!("Got unexpected {:?} as prefix", op); Err(ParseErr::Invalid)}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}.map_err(|err| { debug!("Unexpected error at start of expr: {:?}", err); err })?;
|
||||||
|
debug!("lhs of expression is {:?}, min_bp is {}", lhs, min_bp);
|
||||||
|
loop {
|
||||||
|
debug!("loop start");
|
||||||
|
let op: Op = match self.lex.peek() {
|
||||||
|
Err(ParseErr::Eof) => break,
|
||||||
|
Err(e) => { debug!("In expr got err {:?}", e); Err(*e) },
|
||||||
|
Ok(tok) => match tok {
|
||||||
|
Token::Op(op) => match op {
|
||||||
|
Op::RParen => {
|
||||||
|
debug!("got RParen");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
_ => Ok(*op),
|
||||||
|
}
|
||||||
|
v => { debug!("Got unexpected token {:?}", v); Err(ParseErr::Invalid) },
|
||||||
|
}
|
||||||
|
}.map_err(|err| { debug!("Unexpected error inside expr at {:?}", err); err })?;
|
||||||
|
debug!("op is {:?}", op);
|
||||||
|
if let Some((lbp, rbp)) = op.bp_infix() {
|
||||||
|
if (lbp < min_bp) { break; }
|
||||||
|
self.lex.next();
|
||||||
|
let rhs: Expr = self.parse_expr(rbp)?;
|
||||||
|
lhs = Expr::Node(op, vec![lhs, rhs]);
|
||||||
|
} else {
|
||||||
|
debug!("Got unexpected non-infix operator in expression: {:?}", op);
|
||||||
|
return Err(ParseErr::Invalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("Returning expr {:?}", lhs);
|
||||||
|
Ok(lhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Expr {
|
||||||
|
Evaluated,
|
||||||
|
Atom(Atom),
|
||||||
|
Node(Op, Vec<Expr>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Expr {
|
||||||
|
fn eval(&mut self) -> BigFloat {
|
||||||
|
let res = match self {
|
||||||
|
Expr::Atom(_) => {
|
||||||
|
let v = mem::replace(self, Expr::Evaluated);
|
||||||
|
if let Expr::Atom(at) = v {
|
||||||
|
match at {
|
||||||
|
Atom::Number(n) => n,
|
||||||
|
Atom::Const(c) => c.get_val(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
// at.get_val()
|
||||||
|
}
|
||||||
|
Expr::Node(op, exprs) => {
|
||||||
|
*self = Expr::Atom(Atom::Number(op.apply_to(exprs)));
|
||||||
|
self.eval()
|
||||||
|
}
|
||||||
|
Expr::Evaluated => unreachable!("Tried to evaluate an already evaluated node"),
|
||||||
|
};
|
||||||
|
// debug!("{:?} evaluated to {}", self, res);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
|
@ -343,14 +343,45 @@ pub async fn admin_invitecode(
|
||||||
}
|
}
|
||||||
|
|
||||||
let active_codes = match list_invite_codes(nats.clone(), token.clone(), false).await {
|
let active_codes = match list_invite_codes(nats.clone(), token.clone(), false).await {
|
||||||
Ok(v) => v,
|
Ok(mut v) => {
|
||||||
|
for v in &mut v {
|
||||||
|
if let Some(used_by) = &v.used_by {
|
||||||
|
if used_by.len() > 32 {
|
||||||
|
v.used_by = Some(format!("{}...", &used_by[0..32]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.creator.len() > 32 {
|
||||||
|
v.creator = format!("{}...", &v.creator[0..32]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return e.into_response();
|
return e.into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let used_codes = match list_invite_codes(nats.clone(), token.clone(), true).await {
|
let used_codes = match list_invite_codes(nats.clone(), token.clone(), true).await {
|
||||||
Ok(v) => v.into_iter().map(|mut v| if v.used_at.is_none() { v.used_at = Some(String::from("unset")); v } else { v }).collect(),
|
Ok(v) => v.into_iter().map(|mut v| {
|
||||||
|
if let Some(used_by) = &v.used_by {
|
||||||
|
if used_by.len() > 32 {
|
||||||
|
v.used_by = Some(format!("{}...", &used_by[0..32]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.creator.len() > 32 {
|
||||||
|
v.creator = format!("{}...", &v.creator[0..32]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.used_at.is_none() {
|
||||||
|
v.used_at = Some(String::from("unset"));
|
||||||
|
v
|
||||||
|
} else {
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}).collect(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return e.into_response();
|
return e.into_response();
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ use tracing::{debug, error};
|
||||||
use tracing::log::warn;
|
use tracing::log::warn;
|
||||||
use crate::{Opts, ALPHA, BUILT_ON, GIT_COMMIT, VERSION, YEAR};
|
use crate::{Opts, ALPHA, BUILT_ON, GIT_COMMIT, VERSION, YEAR};
|
||||||
use crate::routes::index::FrontpageAnnouncement;
|
use crate::routes::index::FrontpageAnnouncement;
|
||||||
|
use crate::routes::Themes;
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
struct FullAnnouncement {
|
struct FullAnnouncement {
|
||||||
|
@ -96,7 +97,7 @@ pub struct AnnouncementTemplate {
|
||||||
built_on: String,
|
built_on: String,
|
||||||
year: String,
|
year: String,
|
||||||
alpha: bool,
|
alpha: bool,
|
||||||
theme: String,
|
theme: Themes,
|
||||||
announcement: FullAnnouncement,
|
announcement: FullAnnouncement,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +110,7 @@ pub async fn announcement_full(Extension(nats): Extension<Arc<jetstream::Context
|
||||||
built_on: BUILT_ON.to_string(),
|
built_on: BUILT_ON.to_string(),
|
||||||
year: YEAR.to_string(),
|
year: YEAR.to_string(),
|
||||||
alpha: ALPHA,
|
alpha: ALPHA,
|
||||||
theme: "default".to_string(),
|
theme: Themes::Default,
|
||||||
announcement,
|
announcement,
|
||||||
}.into_response()
|
}.into_response()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -30,7 +30,7 @@ use tokio::sync::Mutex;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
use tracing::log::warn;
|
use tracing::log::warn;
|
||||||
use crate::{BUILT_ON, GIT_COMMIT, Opts, ALPHA, VERSION, WEBSITE_COUNT, YEAR};
|
use crate::{BUILT_ON, GIT_COMMIT, Opts, ALPHA, VERSION, WEBSITE_COUNT, YEAR};
|
||||||
use crate::routes::{authenticate_user, UserInfo};
|
use crate::routes::{authenticate_user, Themes, UserInfo};
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct FrontpageAnnouncement {
|
pub struct FrontpageAnnouncement {
|
||||||
|
@ -102,7 +102,7 @@ pub fn frontpage_error(error: &str, auth_url: String) -> FrontpageTemplate {
|
||||||
year: YEAR.to_string(),
|
year: YEAR.to_string(),
|
||||||
alpha: ALPHA,
|
alpha: ALPHA,
|
||||||
count: WEBSITE_COUNT.load(Ordering::Relaxed),
|
count: WEBSITE_COUNT.load(Ordering::Relaxed),
|
||||||
theme: "default".to_string(),
|
theme: Themes::Default,
|
||||||
announcement: None,
|
announcement: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,7 +185,7 @@ pub struct FrontpageTemplate {
|
||||||
year: String,
|
year: String,
|
||||||
alpha: bool,
|
alpha: bool,
|
||||||
count: u64,
|
count: u64,
|
||||||
theme: String,
|
theme: Themes,
|
||||||
announcement: Option<FrontpageAnnouncement>,
|
announcement: Option<FrontpageAnnouncement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +202,7 @@ pub async fn frontpage(
|
||||||
year: YEAR.to_string(),
|
year: YEAR.to_string(),
|
||||||
alpha: ALPHA,
|
alpha: ALPHA,
|
||||||
count: WEBSITE_COUNT.load(Ordering::Relaxed),
|
count: WEBSITE_COUNT.load(Ordering::Relaxed),
|
||||||
theme: "default".to_string(),
|
theme: Themes::Default,
|
||||||
announcement,
|
announcement,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,7 +217,7 @@ struct IndexTemplate {
|
||||||
year: String,
|
year: String,
|
||||||
alpha: bool,
|
alpha: bool,
|
||||||
count: u64,
|
count: u64,
|
||||||
theme: String,
|
theme: Themes,
|
||||||
announcement: Option<FrontpageAnnouncement>,
|
announcement: Option<FrontpageAnnouncement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,7 +234,7 @@ pub async fn index(
|
||||||
return (jar.remove("token"), frontpage_error(e.as_str(), opts.auth_url.clone())).into_response();
|
return (jar.remove("token"), frontpage_error(e.as_str(), opts.auth_url.clone())).into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let theme = info.theme.clone();
|
let theme = info.get_theme();
|
||||||
|
|
||||||
let announcement = latest_announcement(nats.clone()).await;
|
let announcement = latest_announcement(nats.clone()).await;
|
||||||
|
|
||||||
|
@ -260,7 +260,7 @@ pub async fn index(
|
||||||
year: YEAR.to_string(),
|
year: YEAR.to_string(),
|
||||||
alpha: ALPHA,
|
alpha: ALPHA,
|
||||||
count: WEBSITE_COUNT.load(Ordering::Relaxed),
|
count: WEBSITE_COUNT.load(Ordering::Relaxed),
|
||||||
theme: "default".to_string(),
|
theme: Themes::Default,
|
||||||
announcement,
|
announcement,
|
||||||
}.into_response()
|
}.into_response()
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,8 @@
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use askama_axum::IntoResponse;
|
use askama_axum::IntoResponse;
|
||||||
|
@ -21,7 +22,13 @@ use asklyphe_common::nats::comms::ServiceResponse;
|
||||||
use async_nats::jetstream;
|
use async_nats::jetstream;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tracing::error;
|
use strum::IntoEnumIterator;
|
||||||
|
use strum_macros::EnumIter;
|
||||||
|
use time::macros::utc_datetime;
|
||||||
|
use time::{OffsetDateTime, UtcDateTime};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
const RANDOM_THEME_EPOCH: UtcDateTime = utc_datetime!(2025-03-19 00:00);
|
||||||
|
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
|
@ -30,7 +37,102 @@ pub mod user_settings;
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod announcement;
|
pub mod announcement;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Default, EnumIter, PartialEq, Eq, Copy, Clone)]
|
||||||
|
pub enum Themes {
|
||||||
|
Classic,
|
||||||
|
Dark,
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Freaky,
|
||||||
|
Gloss,
|
||||||
|
Oled,
|
||||||
|
Water,
|
||||||
|
Random
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Themes {
|
||||||
|
pub fn get_all_themes() -> Vec<Themes> {
|
||||||
|
Self::iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_name(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Themes::Classic => {
|
||||||
|
"classic".to_string()
|
||||||
|
}
|
||||||
|
Themes::Dark => {
|
||||||
|
"dark theme".to_string()
|
||||||
|
}
|
||||||
|
Themes::Default => {
|
||||||
|
"default theme".to_string()
|
||||||
|
}
|
||||||
|
Themes::Freaky => {
|
||||||
|
"freaky".to_string()
|
||||||
|
}
|
||||||
|
Themes::Gloss => {
|
||||||
|
"gloss".to_string()
|
||||||
|
}
|
||||||
|
Themes::Oled => {
|
||||||
|
"lights out".to_string()
|
||||||
|
}
|
||||||
|
Themes::Water => {
|
||||||
|
"water".to_string()
|
||||||
|
}
|
||||||
|
Themes::Random => {
|
||||||
|
"random".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn internal_name(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Themes::Classic => {
|
||||||
|
"classic".to_string()
|
||||||
|
}
|
||||||
|
Themes::Dark => {
|
||||||
|
"dark".to_string()
|
||||||
|
}
|
||||||
|
Themes::Default => {
|
||||||
|
"default".to_string()
|
||||||
|
}
|
||||||
|
Themes::Freaky => {
|
||||||
|
"freaky".to_string()
|
||||||
|
}
|
||||||
|
Themes::Gloss => {
|
||||||
|
"gloss".to_string()
|
||||||
|
}
|
||||||
|
Themes::Oled => {
|
||||||
|
"oled".to_string()
|
||||||
|
}
|
||||||
|
Themes::Water => {
|
||||||
|
"water".to_string()
|
||||||
|
}
|
||||||
|
Themes::Random => {
|
||||||
|
"random".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Themes {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"classic" => Ok(Themes::Classic),
|
||||||
|
"dark" => Ok(Themes::Dark),
|
||||||
|
"default" => Ok(Themes::Default),
|
||||||
|
"freaky" => Ok(Themes::Freaky),
|
||||||
|
"gloss" => Ok(Themes::Gloss),
|
||||||
|
"oled" => Ok(Themes::Oled),
|
||||||
|
"water" => Ok(Themes::Water),
|
||||||
|
"random" => Ok(Themes::Random),
|
||||||
|
_ => Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
pub struct UserInfo {
|
pub struct UserInfo {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
@ -39,6 +141,27 @@ pub struct UserInfo {
|
||||||
pub administrator: bool,
|
pub administrator: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl UserInfo {
|
||||||
|
pub fn get_theme(&self) -> Themes {
|
||||||
|
let theme: Themes = self.theme.parse().unwrap_or_default();
|
||||||
|
|
||||||
|
if theme.eq(&Themes::Random) {
|
||||||
|
let possible_themes = Themes::get_all_themes();
|
||||||
|
let current_day = UtcDateTime::now();
|
||||||
|
|
||||||
|
let rand_value = (((current_day - RANDOM_THEME_EPOCH).as_seconds_f64() / 86400.0) % possible_themes.len() as f64) as usize;
|
||||||
|
|
||||||
|
*possible_themes.get(rand_value).unwrap_or(&Themes::Default)
|
||||||
|
} else {
|
||||||
|
theme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_true_theme(&self) -> Themes {
|
||||||
|
self.theme.parse().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn authenticate_user(nats: Arc<jetstream::Context>, token: String) -> Result<UserInfo, String> {
|
pub async fn authenticate_user(nats: Arc<jetstream::Context>, token: String) -> Result<UserInfo, String> {
|
||||||
let response = comms::query_service(
|
let response = comms::query_service(
|
||||||
comms::Query::AuthService(AuthServiceQuery {
|
comms::Query::AuthService(AuthServiceQuery {
|
||||||
|
|
|
@ -12,12 +12,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::routes::index::frontpage_error;
|
use crate::routes::index::frontpage_error;
|
||||||
use crate::routes::{authenticate_user, UserInfo};
|
use crate::routes::{authenticate_user, Themes, UserInfo};
|
||||||
use crate::searchbot::{gather_image_results, gather_search_results};
|
use crate::searchbot::{gather_image_results, gather_search_results};
|
||||||
use crate::unit_converter;
|
use crate::unit_converter;
|
||||||
use crate::unit_converter::UnitConversion;
|
use crate::unit_converter::UnitConversion;
|
||||||
use crate::wikipedia::WikipediaSummary;
|
use crate::wikipedia::WikipediaSummary;
|
||||||
use crate::{wikipedia, Opts, ALPHA, BUILT_ON, GIT_COMMIT, VERSION, YEAR};
|
use crate::{wikipedia, Opts, ALPHA, BUILT_ON, GIT_COMMIT, VERSION, YEAR};
|
||||||
|
use crate::bangs;
|
||||||
|
use crate::math;
|
||||||
|
use crate::math::Calculation;
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use asklyphe_common::nats;
|
use asklyphe_common::nats;
|
||||||
use asklyphe_common::nats::bingservice::{
|
use asklyphe_common::nats::bingservice::{
|
||||||
|
@ -68,6 +71,7 @@ pub struct Complications {
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
wikipedia: Option<WikipediaSummary>,
|
wikipedia: Option<WikipediaSummary>,
|
||||||
unit_converter: Option<UnitConversion>,
|
unit_converter: Option<UnitConversion>,
|
||||||
|
math: Option<Calculation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn search(
|
pub async fn search(
|
||||||
|
@ -111,7 +115,7 @@ struct SearchTemplateJavascript {
|
||||||
built_on: String,
|
built_on: String,
|
||||||
year: String,
|
year: String,
|
||||||
alpha: bool,
|
alpha: bool,
|
||||||
theme: String,
|
theme: Themes,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn search_js(
|
pub async fn search_js(
|
||||||
|
@ -121,7 +125,7 @@ pub async fn search_js(
|
||||||
Extension(opts): Extension<Opts>,
|
Extension(opts): Extension<Opts>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
fn error_response(query: String, info: UserInfo, error: &str) -> SearchTemplateJavascript {
|
fn error_response(query: String, info: UserInfo, error: &str) -> SearchTemplateJavascript {
|
||||||
let theme = info.theme.clone();
|
let theme = info.get_theme();
|
||||||
let querystr = url_encoded_data::stringify(&[("q", query.as_str())]);
|
let querystr = url_encoded_data::stringify(&[("q", query.as_str())]);
|
||||||
SearchTemplateJavascript {
|
SearchTemplateJavascript {
|
||||||
info,
|
info,
|
||||||
|
@ -170,12 +174,22 @@ pub async fn search_js(
|
||||||
unit_query = unit_query.replace("metre", "meter");
|
unit_query = unit_query.replace("metre", "meter");
|
||||||
let unit_comp = unit_converter::convert_unit(&unit_query);
|
let unit_comp = unit_converter::convert_unit(&unit_query);
|
||||||
complications.unit_converter = unit_comp;
|
complications.unit_converter = unit_comp;
|
||||||
|
|
||||||
|
let bang_redirect = bangs::redirect_bang(&query);
|
||||||
|
if let Some(redirect) = bang_redirect {
|
||||||
|
return Redirect::to(&redirect).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut calc_query = query.clone().to_lowercase();
|
||||||
|
calc_query = calc_query.replace("calculate", "").replace("what is", "");
|
||||||
|
let math = math::calculate(&calc_query);
|
||||||
|
complications.math = math;
|
||||||
} else {
|
} else {
|
||||||
complications.disabled = true;
|
complications.disabled = true;
|
||||||
query = query.replace("-complications", "");
|
query = query.replace("-complications", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
let theme = info.theme.clone();
|
let theme = info.get_theme();
|
||||||
let querystr = url_encoded_data::stringify(&[("q", og_query.as_str())]);
|
let querystr = url_encoded_data::stringify(&[("q", og_query.as_str())]);
|
||||||
SearchTemplateJavascript {
|
SearchTemplateJavascript {
|
||||||
info,
|
info,
|
||||||
|
@ -217,7 +231,7 @@ pub struct SearchTemplate {
|
||||||
pub built_on: String,
|
pub built_on: String,
|
||||||
pub year: String,
|
pub year: String,
|
||||||
pub alpha: bool,
|
pub alpha: bool,
|
||||||
pub theme: String,
|
pub theme: Themes,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn search_nojs(
|
pub async fn search_nojs(
|
||||||
|
@ -227,7 +241,7 @@ pub async fn search_nojs(
|
||||||
Extension(opts): Extension<Opts>,
|
Extension(opts): Extension<Opts>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
fn error_response(query: String, info: UserInfo, error: &str) -> SearchTemplate {
|
fn error_response(query: String, info: UserInfo, error: &str) -> SearchTemplate {
|
||||||
let theme = info.theme.clone();
|
let theme = info.get_theme();
|
||||||
let querystr = url_encoded_data::stringify(&[("q", query.as_str())]);
|
let querystr = url_encoded_data::stringify(&[("q", query.as_str())]);
|
||||||
SearchTemplate {
|
SearchTemplate {
|
||||||
info,
|
info,
|
||||||
|
@ -282,6 +296,16 @@ pub async fn search_nojs(
|
||||||
unit_query = unit_query.replace("metre", "meter");
|
unit_query = unit_query.replace("metre", "meter");
|
||||||
let unit_comp = unit_converter::convert_unit(&unit_query);
|
let unit_comp = unit_converter::convert_unit(&unit_query);
|
||||||
complications.unit_converter = unit_comp;
|
complications.unit_converter = unit_comp;
|
||||||
|
|
||||||
|
let bang_redirect = bangs::redirect_bang(&query);
|
||||||
|
if let Some(redirect) = bang_redirect {
|
||||||
|
return Redirect::to(&redirect).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut calc_query = query.clone().to_lowercase();
|
||||||
|
calc_query = calc_query.replace("calculate", "").replace("what is", "");
|
||||||
|
let math = math::calculate(&calc_query);
|
||||||
|
complications.math = math;
|
||||||
} else {
|
} else {
|
||||||
complications.disabled = true;
|
complications.disabled = true;
|
||||||
query = query.replace("-complications", "");
|
query = query.replace("-complications", "");
|
||||||
|
@ -416,7 +440,7 @@ pub struct ImageSearchTemplate {
|
||||||
pub built_on: String,
|
pub built_on: String,
|
||||||
pub year: String,
|
pub year: String,
|
||||||
pub alpha: bool,
|
pub alpha: bool,
|
||||||
pub theme: String,
|
pub theme: Themes,
|
||||||
}
|
}
|
||||||
pub async fn image_search(
|
pub async fn image_search(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
|
@ -425,7 +449,7 @@ pub async fn image_search(
|
||||||
Extension(opts): Extension<Opts>,
|
Extension(opts): Extension<Opts>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
fn error_response(query: String, info: UserInfo, error: &str) -> ImageSearchTemplate {
|
fn error_response(query: String, info: UserInfo, error: &str) -> ImageSearchTemplate {
|
||||||
let theme = info.theme.clone();
|
let theme = info.get_theme();
|
||||||
let querystr = url_encoded_data::stringify(&[("q", query.as_str())]);
|
let querystr = url_encoded_data::stringify(&[("q", query.as_str())]);
|
||||||
ImageSearchTemplate {
|
ImageSearchTemplate {
|
||||||
info,
|
info,
|
||||||
|
|
|
@ -27,49 +27,12 @@ use serde::Deserialize;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
use crate::{BUILT_ON, GIT_COMMIT, Opts, ALPHA, VERSION, WEBSITE_COUNT, YEAR};
|
use crate::{BUILT_ON, GIT_COMMIT, Opts, ALPHA, VERSION, WEBSITE_COUNT, YEAR};
|
||||||
use crate::routes::{authenticate_user, UserInfo};
|
use crate::routes::{authenticate_user, Themes, UserInfo};
|
||||||
|
|
||||||
pub struct Theme<'a> {
|
|
||||||
pub value: &'a str,
|
|
||||||
pub name: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub static THEMES: &[Theme] = &[
|
|
||||||
Theme {
|
|
||||||
value: "default",
|
|
||||||
name: "default theme",
|
|
||||||
},
|
|
||||||
Theme {
|
|
||||||
value: "dark",
|
|
||||||
name: "dark theme",
|
|
||||||
},
|
|
||||||
Theme {
|
|
||||||
value: "oled",
|
|
||||||
name: "lights out",
|
|
||||||
},
|
|
||||||
Theme {
|
|
||||||
value: "classic",
|
|
||||||
name: "classic",
|
|
||||||
},
|
|
||||||
Theme {
|
|
||||||
value: "freaky",
|
|
||||||
name: "freaky",
|
|
||||||
},
|
|
||||||
Theme {
|
|
||||||
value: "water",
|
|
||||||
name: "water",
|
|
||||||
},
|
|
||||||
Theme {
|
|
||||||
value: "gloss",
|
|
||||||
name: "gloss",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "user_settings.html")]
|
#[template(path = "user_settings.html")]
|
||||||
pub struct SettingsTemplate {
|
pub struct SettingsTemplate {
|
||||||
themes: &'static [Theme<'static>],
|
themes: Vec<Themes>,
|
||||||
|
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
|
|
||||||
info: UserInfo,
|
info: UserInfo,
|
||||||
|
@ -80,7 +43,8 @@ pub struct SettingsTemplate {
|
||||||
year: String,
|
year: String,
|
||||||
alpha: bool,
|
alpha: bool,
|
||||||
count: u64,
|
count: u64,
|
||||||
theme: String,
|
theme: Themes,
|
||||||
|
true_theme: Themes,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user_settings(
|
pub async fn user_settings(
|
||||||
|
@ -96,11 +60,11 @@ pub async fn user_settings(
|
||||||
return (jar.remove("token"), crate::routes::index::frontpage_error(e.as_str(), opts.auth_url.clone())).into_response();
|
return (jar.remove("token"), crate::routes::index::frontpage_error(e.as_str(), opts.auth_url.clone())).into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let theme = info.theme.clone();
|
let theme = info.get_theme();
|
||||||
SettingsTemplate {
|
SettingsTemplate {
|
||||||
themes: THEMES,
|
themes: Themes::get_all_themes(),
|
||||||
error: None,
|
error: None,
|
||||||
info,
|
info: info.clone(),
|
||||||
search_query: "".to_string(),
|
search_query: "".to_string(),
|
||||||
version: VERSION.to_string(),
|
version: VERSION.to_string(),
|
||||||
git_commit: GIT_COMMIT.to_string(),
|
git_commit: GIT_COMMIT.to_string(),
|
||||||
|
@ -109,6 +73,7 @@ pub async fn user_settings(
|
||||||
alpha: ALPHA,
|
alpha: ALPHA,
|
||||||
count: WEBSITE_COUNT.load(Ordering::Relaxed),
|
count: WEBSITE_COUNT.load(Ordering::Relaxed),
|
||||||
theme,
|
theme,
|
||||||
|
true_theme: info.get_true_theme(),
|
||||||
}.into_response()
|
}.into_response()
|
||||||
} else {
|
} else {
|
||||||
Redirect::temporary("/").into_response()
|
Redirect::temporary("/").into_response()
|
||||||
|
@ -126,11 +91,11 @@ pub async fn theme_change_post(
|
||||||
Extension(opts): Extension<Opts>,
|
Extension(opts): Extension<Opts>,
|
||||||
Form(input): Form<ThemeChangeForm>,
|
Form(input): Form<ThemeChangeForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
fn settings_error(info: UserInfo, theme: String, error: String) -> impl IntoResponse {
|
fn settings_error(info: UserInfo, theme: Themes, error: String) -> impl IntoResponse {
|
||||||
SettingsTemplate {
|
SettingsTemplate {
|
||||||
themes: THEMES,
|
themes: Themes::get_all_themes(),
|
||||||
error: Some(error),
|
error: Some(error),
|
||||||
info,
|
info: info.clone(),
|
||||||
search_query: "".to_string(),
|
search_query: "".to_string(),
|
||||||
version: VERSION.to_string(),
|
version: VERSION.to_string(),
|
||||||
git_commit: GIT_COMMIT.to_string(),
|
git_commit: GIT_COMMIT.to_string(),
|
||||||
|
@ -139,6 +104,7 @@ pub async fn theme_change_post(
|
||||||
alpha: ALPHA,
|
alpha: ALPHA,
|
||||||
count: WEBSITE_COUNT.load(Ordering::Relaxed),
|
count: WEBSITE_COUNT.load(Ordering::Relaxed),
|
||||||
theme,
|
theme,
|
||||||
|
true_theme: info.get_true_theme(),
|
||||||
}.into_response()
|
}.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,10 +117,12 @@ pub async fn theme_change_post(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let theme = info.theme.clone();
|
let theme = info.get_theme();
|
||||||
|
|
||||||
if !THEMES.iter().map(|v| v.value.to_string()).collect::<Vec<String>>().contains(&input.theme.clone().unwrap_or("default".to_string())) {
|
if let Some(theme_input) = &input.theme {
|
||||||
return settings_error(info, theme, "invalid input, please try again!".to_string()).into_response();
|
if !Themes::get_all_themes().iter().map(|x| x.internal_name().to_string()).collect::<Vec<String>>().contains(&theme_input) {
|
||||||
|
return settings_error(info, theme.clone(), "invalid input, please try again!".to_string()).into_response();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = comms::query_service(comms::Query::AuthService(AuthServiceQuery {
|
let response = comms::query_service(comms::Query::AuthService(AuthServiceQuery {
|
||||||
|
|
|
@ -341,7 +341,7 @@ pub async fn gather_search_results(nats: Arc<jetstream::Context>, query: &str, u
|
||||||
search_results.remove(rm - i);
|
search_results.remove(rm - i);
|
||||||
}
|
}
|
||||||
|
|
||||||
let theme = user_info.theme.clone();
|
let theme = user_info.get_theme();
|
||||||
let querystr = url_encoded_data::stringify(&[("q", query)]);
|
let querystr = url_encoded_data::stringify(&[("q", query)]);
|
||||||
SearchTemplate {
|
SearchTemplate {
|
||||||
info: user_info,
|
info: user_info,
|
||||||
|
@ -489,7 +489,7 @@ pub async fn gather_image_results(nats: Arc<jetstream::Context>, query: &str, us
|
||||||
result.src = format!("/imgproxy?{}", url);
|
result.src = format!("/imgproxy?{}", url);
|
||||||
}
|
}
|
||||||
|
|
||||||
let theme = user_info.theme.clone();
|
let theme = user_info.get_theme();
|
||||||
ImageSearchTemplate {
|
ImageSearchTemplate {
|
||||||
info: user_info,
|
info: user_info,
|
||||||
error: None,
|
error: None,
|
||||||
|
|
11
asklyphe-frontend/static/themes/classic/imagesearch.css
Normal file
11
asklyphe-frontend/static/themes/classic/imagesearch.css
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.pagecontent {
|
||||||
|
background: url("/static/snow.gif");
|
||||||
|
border: 3px inset black;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 800px) {
|
||||||
|
.pagecontent {
|
||||||
|
width: calc(100% - 4px);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "admin/shell.html" %}
|
{% extends "admin/shell.html" %}
|
||||||
|
|
||||||
{% block title %}Home{% endblock %}
|
{% block title %}Invite Codes{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block title %}{{ announcement.title }}{% endblock %}
|
{% block title %}{{ announcement.title }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" href="/static/themes/default/announcement.css"/>
|
<link rel="stylesheet" href="/static/themes/default/announcement.css?git={{ git_commit }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
|
@ -12,11 +12,11 @@
|
||||||
<a href="/" class="lyphe">
|
<a href="/" class="lyphe">
|
||||||
<div id="lyphesmall">
|
<div id="lyphesmall">
|
||||||
<div class="bent-surrounding">
|
<div class="bent-surrounding">
|
||||||
<img id="lypheimg" src="/static/img/lyphebent{% if alpha %}-alpha{% endif %}.png"
|
<img id="lypheimg" src="/static/img/lyphebent{% if alpha %}-alpha{% endif %}.png?git={{ git_commit }}"
|
||||||
alt="image of lyphe, our mascot!">
|
alt="image of lyphe, our mascot!">
|
||||||
</div>
|
</div>
|
||||||
<div id="lyphetitlenav">
|
<div id="lyphetitlenav">
|
||||||
<img src="/static/img/logo.png" alt="ask lyphe!"/>
|
<img src="/static/img/logo.png?git={{ git_commit }}" alt="ask lyphe!"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
{% block title %}the best search engine{% endblock %}
|
{% block title %}the best search engine{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" href="/static/themes/{{theme}}/frontpage.css"/>
|
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/frontpage.css?git={{ git_commit }}"/>
|
||||||
<link rel="stylesheet" href="/static/themes/{{theme}}/inline-announcement.css"/>
|
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/inline-announcement.css?git={{ git_commit }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
|
@ -17,10 +17,10 @@
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
<div id="lyphebig">
|
<div id="lyphebig">
|
||||||
<div class="bent-surrounding">
|
<div class="bent-surrounding">
|
||||||
<img id="lypheimg" src="/static/img/lyphebent{% if alpha %}-alpha{% endif %}.png" alt="image of lyphe, our mascot!">
|
<img id="lypheimg" src="/static/img/lyphebent{% if alpha %}-alpha{% endif %}.png?git={{ git_commit }}" alt="image of lyphe, our mascot!">
|
||||||
</div>
|
</div>
|
||||||
<div id="lyphetitle">
|
<div id="lyphetitle">
|
||||||
<img src="/static/img/logo.png" alt="ask Lyphe!"/>
|
<img src="/static/img/logo.png?git={{ git_commit }}" alt="ask Lyphe!"/>
|
||||||
<p>a user-first, customizable, useful, and fun search engine!</p>
|
<p>a user-first, customizable, useful, and fun search engine!</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -67,7 +67,7 @@
|
||||||
<div class="pagegradient">
|
<div class="pagegradient">
|
||||||
<div id="userfirst" class="feature">
|
<div id="userfirst" class="feature">
|
||||||
<h1>user-first</h1>
|
<h1>user-first</h1>
|
||||||
<img src="/static/lyphe.png" alt="lorem ipsum dolor sit amet"/>
|
<img src="/static/lyphe.png?git={{ git_commit }}" alt="lorem ipsum dolor sit amet"/>
|
||||||
<p>
|
<p>
|
||||||
we will never serve a single ad.
|
we will never serve a single ad.
|
||||||
<br/>
|
<br/>
|
||||||
|
@ -90,7 +90,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="customizable" class="feature">
|
<div id="customizable" class="feature">
|
||||||
<h1>customizable</h1>
|
<h1>customizable</h1>
|
||||||
<img src="/static/lyphe.png" alt="lorem ipsum dolor sit amet"/>
|
<img src="/static/lyphe.png?git={{ git_commit }}" alt="lorem ipsum dolor sit amet"/>
|
||||||
<p>
|
<p>
|
||||||
we aim to design askLyphe in a way that makes personalizing your search results easy and fun!
|
we aim to design askLyphe in a way that makes personalizing your search results easy and fun!
|
||||||
<br/>
|
<br/>
|
||||||
|
@ -102,7 +102,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="useful-fun" class="feature">
|
<div id="useful-fun" class="feature">
|
||||||
<h1>useful, but fun!</h1>
|
<h1>useful, but fun!</h1>
|
||||||
<img src="/static/lyphe.png" alt="lorem ipsum dolor sit amet"/>
|
<img src="/static/lyphe.png?git={{ git_commit }}" alt="lorem ipsum dolor sit amet"/>
|
||||||
<p>
|
<p>
|
||||||
our search engine does not rely on (however may still include, for completeness) results from
|
our search engine does not rely on (however may still include, for completeness) results from
|
||||||
Google, Bing, or any other search engine out there.
|
Google, Bing, or any other search engine out there.
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="fourth-box" class="feature">
|
<div id="fourth-box" class="feature">
|
||||||
<h1>fourth box!</h1>
|
<h1>fourth box!</h1>
|
||||||
<img src="/static/lyphe.png" alt="lorem ipsum dolor sit amet"/>
|
<img src="/static/lyphe.png?git={{ git_commit }}" alt="lorem ipsum dolor sit amet"/>
|
||||||
<p>
|
<p>
|
||||||
i haven't decided what to put here yet! we're still in alpha so i'm sure i'll come up with something
|
i haven't decided what to put here yet! we're still in alpha so i'm sure i'll come up with something
|
||||||
eventually, but i wanted this fourth box because i feel like the design flows nicer with it (:
|
eventually, but i wanted this fourth box because i feel like the design flows nicer with it (:
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
{% block title %}Home{% endblock %}
|
{% block title %}Home{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" href="/static/themes/default/home.css"/>
|
<link rel="stylesheet" href="/static/themes/default/home.css?git={{ git_commit }}"/>
|
||||||
<link rel="stylesheet" href="/static/themes/{{theme}}/home.css"/>
|
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/home.css?git={{ git_commit }}"/>
|
||||||
<link rel="stylesheet" href="/static/themes/{{theme}}/inline-announcement.css"/>
|
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/inline-announcement.css?git={{ git_commit }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
|
@ -15,10 +15,10 @@
|
||||||
<div class="pagecontent">
|
<div class="pagecontent">
|
||||||
<div id="lyphebig">
|
<div id="lyphebig">
|
||||||
<div class="bent-surrounding">
|
<div class="bent-surrounding">
|
||||||
<img id="lypheimg" src="/static/img/lyphebent{% if alpha %}-alpha{% endif %}.png" alt="image of lyphe, our mascot!">
|
<img id="lypheimg" src="/static/img/lyphebent{% if alpha %}-alpha{% endif %}.png?git={{ git_commit }}" alt="image of lyphe, our mascot!">
|
||||||
</div>
|
</div>
|
||||||
<div id="lyphetitle">
|
<div id="lyphetitle">
|
||||||
<img src="/static/img/logo.png" alt="ask Lyphe!"/>
|
<img src="/static/img/logo.png?git={{ git_commit }}" alt="ask Lyphe!"/>
|
||||||
<h2>the best search engine!</h2>
|
<h2>the best search engine!</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
{% block title %}Images - {{ search_query }}{% endblock %}
|
{% block title %}Images - {{ search_query }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" href="/static/themes/default/imagesearch.css"/>
|
<link rel="stylesheet" href="/static/themes/default/imagesearch.css?git={{ git_commit }}"/>
|
||||||
<link rel="stylesheet" href="/static/themes/{{theme}}/imagesearch.css"/>
|
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/imagesearch.css?git={{ git_commit }}"/>
|
||||||
{% if search_query == "notnite" %}<link rel="stylesheet" href="/static/creature.css"/>{% endif %}
|
{% if search_query == "notnite" %}<link rel="stylesheet" href="/static/creature.css?git={{ git_commit }}"/>{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
|
@ -16,10 +16,10 @@
|
||||||
|
|
||||||
<div class="index-bar">
|
<div class="index-bar">
|
||||||
<a href="{{ websearch_url }}">
|
<a href="{{ websearch_url }}">
|
||||||
<img id="websearch-img" src="/static/img/websearch.png" alt="search the web"/>
|
<img id="websearch-img" src="/static/img/websearch.png?git={{ git_commit }}" alt="search the web"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ imagesearch_url }}">
|
<a href="{{ imagesearch_url }}">
|
||||||
<img id="imagesearch-img-selected" src="/static/img/imagesearch_selected.png" alt="search for images"/>
|
<img id="imagesearch-img-selected" src="/static/img/imagesearch_selected.png?git={{ git_commit }}" alt="search for images"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
{% block title %}{{ search_query }}{% endblock %}
|
{% block title %}{{ search_query }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" href="/static/themes/default/search.css"/>
|
<link rel="stylesheet" href="/static/themes/default/search.css?git={{ git_commit }}"/>
|
||||||
<link rel="stylesheet" href="/static/themes/{{theme}}/search.css"/>
|
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/search.css?git={{ git_commit }}"/>
|
||||||
{% if search_query == "notnite" %}<link rel="stylesheet" href="/static/creature.css"/>{% endif %}
|
{% if search_query == "notnite" %}<link rel="stylesheet" href="/static/creature.css?git={{ git_commit }}"/>{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
|
@ -16,10 +16,10 @@
|
||||||
|
|
||||||
<div class="index-bar">
|
<div class="index-bar">
|
||||||
<a href="{{ websearch_url }}">
|
<a href="{{ websearch_url }}">
|
||||||
<img id="websearch-img-selected" src="/static/img/websearch_selected.png" alt="search the web"/>
|
<img id="websearch-img-selected" src="/static/img/websearch_selected.png?git={{ git_commit }}" alt="search the web"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ imagesearch_url }}">
|
<a href="{{ imagesearch_url }}">
|
||||||
<img id="imagesearch-img" src="/static/img/imagesearch.png" alt="search for images"/>
|
<img id="imagesearch-img" src="/static/img/imagesearch.png?git={{ git_commit }}" alt="search for images"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -75,6 +75,13 @@
|
||||||
</div>
|
</div>
|
||||||
{% when None %}
|
{% when None %}
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
|
{% match complications.math %}
|
||||||
|
{% when Some with (math) %}
|
||||||
|
<div class="calculator-complication">
|
||||||
|
<h1>{{ math.equation }} = {{ math.result }}</h1>
|
||||||
|
</div>
|
||||||
|
{% when None %}
|
||||||
|
{% endmatch %}
|
||||||
</div>
|
</div>
|
||||||
<ol class="search-result-list">
|
<ol class="search-result-list">
|
||||||
{% for result in search_results %}
|
{% for result in search_results %}
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
{% block title %}{{ search_query }}{% endblock %}
|
{% block title %}{{ search_query }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" href="/static/themes/default/search.css"/>
|
<link rel="stylesheet" href="/static/themes/default/search.css?git={{ git_commit }}"/>
|
||||||
<link rel="stylesheet" href="/static/themes/{{theme}}/search.css"/>
|
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/search.css?git={{ git_commit }}"/>
|
||||||
{% if search_query == "notnite" %}<link rel="stylesheet" href="/static/creature.css"/>{% endif %}
|
{% if search_query == "notnite" %}<link rel="stylesheet" href="/static/creature.css?git={{ git_commit }}"/>{% endif %}
|
||||||
<script src="/static/js/search.js" defer></script>
|
<script src="/static/js/search.js?git={{ git_commit }}" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
|
@ -17,10 +17,10 @@
|
||||||
|
|
||||||
<div class="index-bar">
|
<div class="index-bar">
|
||||||
<a href="{{ websearch_url }}">
|
<a href="{{ websearch_url }}">
|
||||||
<img id="websearch-img-selected" src="/static/img/websearch_selected.png" alt="search the web"/>
|
<img id="websearch-img-selected" src="/static/img/websearch_selected.png?git={{ git_commit }}" alt="search the web"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ imagesearch_url }}">
|
<a href="{{ imagesearch_url }}">
|
||||||
<img id="imagesearch-img" src="/static/img/imagesearch.png" alt="search for images"/>
|
<img id="imagesearch-img" src="/static/img/imagesearch.png?git={{ git_commit }}" alt="search for images"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -67,6 +67,13 @@
|
||||||
</div>
|
</div>
|
||||||
{% when None %}
|
{% when None %}
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
|
{% match complications.math %}
|
||||||
|
{% when Some with (math) %}
|
||||||
|
<div class="calculator-complication">
|
||||||
|
<h1>{{ math.equation }} = {{ math.result }}</h1>
|
||||||
|
</div>
|
||||||
|
{% when None %}
|
||||||
|
{% endmatch %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
|
|
|
@ -11,8 +11,8 @@
|
||||||
title="AskLyphe"
|
title="AskLyphe"
|
||||||
href="/static/osd.xml" />
|
href="/static/osd.xml" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/themes/default/shell.css" />
|
<link rel="stylesheet" href="/static/themes/default/shell.css?git={{ git_commit }}" />
|
||||||
<link rel="stylesheet" href="/static/themes/{{theme}}/shell.css" />
|
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/shell.css?git={{ git_commit }}" />
|
||||||
|
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
{% block title %}{{ search_query }}{% endblock %}
|
{% block title %}{{ search_query }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" href="/static/themes/default/settings.css"/>
|
<link rel="stylesheet" href="/static/themes/default/settings.css?git={{ git_commit }}"/>
|
||||||
<link rel="stylesheet" href="/static/themes/{{theme}}/settings.css"/>
|
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/settings.css?git={{ git_commit }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
|
@ -43,16 +43,15 @@
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div id="theme" class="settings-section">
|
<div id="theme" class="settings-section">
|
||||||
<h2>theme</h2>
|
<h2>theme</h2>
|
||||||
{% for t in themes %}
|
<p>your current theme is: "{{true_theme.display_name()}}"</p>
|
||||||
{%if theme==t.value%}
|
{% if true_theme.internal_name() != theme.internal_name() %}
|
||||||
<p>your current theme is: "{{t.name}}"</p>
|
<p>today's random theme is {{ theme.display_name() }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
|
||||||
<form action="/user_settings/set_theme" method="post">
|
<form action="/user_settings/set_theme" method="post">
|
||||||
<label for="theme-selector">theme</label>
|
<label for="theme-selector">theme</label>
|
||||||
<select name="theme" id="theme-selector">
|
<select name="theme" id="theme-selector">
|
||||||
{% for t in themes %}
|
{% for t in themes %}
|
||||||
<option value="{{t.value}}" {%if theme==t.value%}selected{%endif%}>{{t.name}}</option>
|
<option value="{{t.internal_name()}}" {%if true_theme.internal_name()==t.internal_name()%}selected{%endif%}>{{t.display_name()}}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" id="theme-submit">change theme!</button>
|
<button type="submit" id="theme-submit">change theme!</button>
|
||||||
|
|
|
@ -457,7 +457,8 @@ pub async fn user_count(db: &DatabaseConnection) -> Result<usize, FetchUserError
|
||||||
/// returns the number of users in the database who are admins
|
/// returns the number of users in the database who are admins
|
||||||
pub async fn admin_count(db: &DatabaseConnection) -> Result<usize, FetchUserError> {
|
pub async fn admin_count(db: &DatabaseConnection) -> Result<usize, FetchUserError> {
|
||||||
// dont fucking touch this, i don't know why it works but it does, it's actually evil
|
// dont fucking touch this, i don't know why it works but it does, it's actually evil
|
||||||
Ok(user::Entity::find().filter(user::Column::Flags.into_expr().binary(BinOper::LShift, Expr::value(63 - 2)).lt(1 << (63 - 2)))
|
// note: doesn't work
|
||||||
|
Ok(user::Entity::find().filter(user::Column::Flags.into_expr().binary(BinOper::BitAnd, UserFlag::Administrator as i64).binary(BinOper::NotEqual, 0))
|
||||||
.count(db).await.map_err(|e| {
|
.count(db).await.map_err(|e| {
|
||||||
error!("DATABASE ERROR WHILE ADMINCOUNT: {e}");
|
error!("DATABASE ERROR WHILE ADMINCOUNT: {e}");
|
||||||
FetchUserError::DatabaseError
|
FetchUserError::DatabaseError
|
||||||
|
|
|
@ -15,6 +15,7 @@ mod process;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
mod email;
|
mod email;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
|
||||||
|
@ -36,6 +37,8 @@ pub static SMTP_URL: Lazy<String> = Lazy::new(|| std::env::var("SMTP_URL").expec
|
||||||
pub static SMTP_USERNAME: Lazy<String> = Lazy::new(|| std::env::var("SMTP_USERNAME").expect("NO SMTP_USERNAME DEFINED"));
|
pub static SMTP_USERNAME: Lazy<String> = Lazy::new(|| std::env::var("SMTP_USERNAME").expect("NO SMTP_USERNAME DEFINED"));
|
||||||
pub static SMTP_PASSWORD: Lazy<String> = Lazy::new(|| std::env::var("SMTP_PASSWORD").expect("NO SMTP_PASSWORD DEFINED"));
|
pub static SMTP_PASSWORD: Lazy<String> = Lazy::new(|| std::env::var("SMTP_PASSWORD").expect("NO SMTP_PASSWORD DEFINED"));
|
||||||
|
|
||||||
|
pub static AUTH_URL: Lazy<String> = Lazy::new(|| std::env::var("AUTH_URL").unwrap_or("https://auth.asklyphe.com".to_string()));
|
||||||
|
|
||||||
pub static PROCESSES_HANDLED: AtomicU64 = AtomicU64::new(0);
|
pub static PROCESSES_HANDLED: AtomicU64 = AtomicU64::new(0);
|
||||||
pub static LAST_MESSAGE: AtomicI64 = AtomicI64::new(0);
|
pub static LAST_MESSAGE: AtomicI64 = AtomicI64::new(0);
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ use crate::db::{invite_code, session, user};
|
||||||
use crate::db::invite_code::ConsumeInviteCodeError;
|
use crate::db::invite_code::ConsumeInviteCodeError;
|
||||||
use crate::db::session::{CreateSessionError, DeleteSessionError, FetchSessionError};
|
use crate::db::session::{CreateSessionError, DeleteSessionError, FetchSessionError};
|
||||||
use crate::db::user::{CreateUserError, DeleteUserError, EmailChangeError, FetchUserError, RegisterVerificationCodeError, VerifyEmailPassComboError, VerifyVerificationCodeError};
|
use crate::db::user::{CreateUserError, DeleteUserError, EmailChangeError, FetchUserError, RegisterVerificationCodeError, VerifyEmailPassComboError, VerifyVerificationCodeError};
|
||||||
use crate::email;
|
use crate::{email, AUTH_URL};
|
||||||
|
|
||||||
fn generate_email_verification_code() -> String {
|
fn generate_email_verification_code() -> String {
|
||||||
rand::thread_rng()
|
rand::thread_rng()
|
||||||
|
@ -121,7 +121,7 @@ pub async fn register_request(db: &DatabaseConnection, request: RegisterRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("verification code for {} is \"{}\"", request.username, verification_code);
|
debug!("verification code for {} is \"{}\"", request.username, verification_code);
|
||||||
email::send_verification_code_email(&request.email, &request.username, format!("https://auth.asklyphe.com/verify?username={}&token={}", request.username, verification_code).as_str());
|
email::send_verification_code_email(&request.email, &request.username, format!("{}/verify?username={}&token={}", AUTH_URL.as_str(), request.username, verification_code).as_str());
|
||||||
|
|
||||||
RegisterResponse::Success
|
RegisterResponse::Success
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ pub static PROXIES: Lazy<Vec<Proxy>> = Lazy::new(|| {
|
||||||
let contents = std::fs::read_to_string(proxy_file);
|
let contents = std::fs::read_to_string(proxy_file);
|
||||||
let mut proxies = vec![];
|
let mut proxies = vec![];
|
||||||
|
|
||||||
for line in contents.expect("FAILED TO READ FILE").lines() {
|
for line in contents.expect("FAILED TO READ FILE").lines().filter(|l| l.len() > 0) {
|
||||||
proxies.push(Proxy::from_str(line).expect("INVALID PROXY"));
|
proxies.push(Proxy::from_str(line).expect("INVALID PROXY"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ impl FromStr for Proxy {
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
let mut parts = s.split(':');
|
let mut parts = s.split(':');
|
||||||
|
let protocol = parts.next().ok_or(ProxyError::InvalidProxyFormat)?;
|
||||||
let host = parts.next().ok_or(ProxyError::InvalidProxyFormat)?;
|
let host = parts.next().ok_or(ProxyError::InvalidProxyFormat)?;
|
||||||
let port = parts.next().ok_or(ProxyError::InvalidProxyFormat)?;
|
let port = parts.next().ok_or(ProxyError::InvalidProxyFormat)?;
|
||||||
let auth = if let Some(user) = parts.next() {
|
let auth = if let Some(user) = parts.next() {
|
||||||
|
@ -39,7 +40,7 @@ impl FromStr for Proxy {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
Ok(Proxy {
|
Ok(Proxy {
|
||||||
address: format!("{}:{}", host, port),
|
address: format!("{}://{}:{}", protocol, host, port),
|
||||||
credentials: auth,
|
credentials: auth,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
34
cert.pem
Normal file
34
cert.pem
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIF7zCCA9egAwIBAgIUb46GxLSqbrjV/nlD+ovwlYcyzOcwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwgYYxCzAJBgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcM
|
||||||
|
CENpdHlOYW1lMRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UECwwSQ29tcGFu
|
||||||
|
eVNlY3Rpb25OYW1lMR0wGwYDVQQDDBRDb21tb25OYW1lT3JIb3N0bmFtZTAeFw0y
|
||||||
|
NTA2MjEwNTA1NTlaFw0zNTA2MTkwNTA1NTlaMIGGMQswCQYDVQQGEwJYWDESMBAG
|
||||||
|
A1UECAwJU3RhdGVOYW1lMREwDwYDVQQHDAhDaXR5TmFtZTEUMBIGA1UECgwLQ29t
|
||||||
|
cGFueU5hbWUxGzAZBgNVBAsMEkNvbXBhbnlTZWN0aW9uTmFtZTEdMBsGA1UEAwwU
|
||||||
|
Q29tbW9uTmFtZU9ySG9zdG5hbWUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
|
||||||
|
AoICAQC28URbBWcTpvOar679u4CwsAHQ+i+9iPBvjRG/ShdvXkAgWm+t/BvKi9JG
|
||||||
|
FOAn49IKOpcteY15qvDFRPDKk8YWoiwdMQSKRNEwEow3YlIs6xX94+PNdwsjaqy/
|
||||||
|
mhJTMh0xrElZJ5+B4mDXQHOzdS6fe0SlNhqEAkFaIuUNX1NAks7yRnkC5LGkSHHj
|
||||||
|
gD2ZThwyZ+cstvT7WEUN9uMz/FfLuQQLrVZDydE9tsoQo0CIl1l0NLiE0BN5RIwi
|
||||||
|
i6Gkao74jlxh6tXv7XcOTxZ1aV3F92qMKN1NtWFEqpC2PDdfLG5iAlwamKguD24N
|
||||||
|
RMDC9CGCiutE4lRhRQWkC89NSxOkG25MGRvK0jut7MiKOia/Xk5uJI2Rid9IWFKv
|
||||||
|
xnuT5AW9PCbjM0OSkw2G0PzthUAO1jrOyA2R50oh/YGsdslELhlpZSiu/StSx+0U
|
||||||
|
x0E9dcQHvnlllU5BrYXbDkoSCiejhD4xV35KmhIwtz7pr2cajfeJ5r7Em/hSBVbS
|
||||||
|
Zqbv5OmGgxTSSDLUTaLJA015vLnLNCV5al/iGzXKl1FOwTIzRLv+og/jK70rwOGX
|
||||||
|
Red2JnKntqfBEnR51gky9axyyz3dAMEE1rCc+oOv7ycZoEKwPdXiyneOCLT40QT6
|
||||||
|
No1UrMJCa32a4+YJgbANB8igFwwhdapD5N+qvpCWtiKsdnbPeQIDAQABo1MwUTAd
|
||||||
|
BgNVHQ4EFgQUq3zHWtMlNawKBIPiOECsuPWTB7IwHwYDVR0jBBgwFoAUq3zHWtMl
|
||||||
|
NawKBIPiOECsuPWTB7IwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
|
||||||
|
AgEAtoERmowTDvnZFV4ETGmj3mPXNLXaqSUDRAB5ol9N4NlRU/vFjI70sYV7uco9
|
||||||
|
628Wc5SLbA66wSmpnq5t1MeTjjra6o6bj5wOKixo1gmpUq7Ru1JmVWsNIo/Zv4ew
|
||||||
|
rmCxuapHL0U9R0P1qpJe0tYzSMFikgkFmHyL86HcuW8B/Ps3bGaGlamqmIK3HlP4
|
||||||
|
Ib+sOtInNZ1Td8eYTIYO5Nk5ko/4rjZrv9+3aZdjde1z/M2VduGGH8TCExD55Rbv
|
||||||
|
+UL8fGIEkfSNTeiA5SAN3dfqcra703mfOJfjJeoubfk8KowfGb/bVKv8Z8rkEDOj
|
||||||
|
so+sOgbq1xnSaQov7WRqYZ0yKZA0u8Arc0L8CX/rwgwoBkQafySEI/32Mqt0R4/w
|
||||||
|
MkmGZLSFTcIFrQVE+wBHTilQ1PfUmAA6kh7ks7SGwlc6KxTAtYZHWklCqib0efaJ
|
||||||
|
AbODBc97vLrR5qoH0VFSGLnjDVEYHb6TREqsCZR+9EP/JcsTRJ8RTeDVg8RnN2a6
|
||||||
|
uy01L7A3d1xnXPux45fpwgGTOEig2sD0BTHZW/bl53xQr8gJvwyr78cIVmycT/6N
|
||||||
|
K0AmYBPQWZLf6rxtommjMgf2DtvhPm6VrbHV7epk8cw8tOVRPD5uLjZzKxgFoZez
|
||||||
|
ZYNjSUse3ChC7l4FhjmTiI5DWOrS/qYbWYi9rzvG6QZwHss=
|
||||||
|
-----END CERTIFICATE-----
|
129
docker-compose.yml
Normal file
129
docker-compose.yml
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
services:
|
||||||
|
auth-frontend:
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- lyphenet
|
||||||
|
env_file: ".env"
|
||||||
|
depends_on:
|
||||||
|
- nats
|
||||||
|
- authservice
|
||||||
|
build:
|
||||||
|
dockerfile: Dockerfile.auth-frontend
|
||||||
|
volumes:
|
||||||
|
- auth_frontend_data:/data
|
||||||
|
image: asklyphe/auth-frontend
|
||||||
|
authservice:
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- lyphenet
|
||||||
|
env_file: ".env"
|
||||||
|
depends_on:
|
||||||
|
- nats
|
||||||
|
build:
|
||||||
|
dockerfile: Dockerfile.authservice
|
||||||
|
image: asklyphe/authservice
|
||||||
|
frontend:
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- lyphenet
|
||||||
|
env_file: ".env"
|
||||||
|
depends_on:
|
||||||
|
- nats
|
||||||
|
build:
|
||||||
|
dockerfile: Dockerfile.frontend
|
||||||
|
volumes:
|
||||||
|
- frontend_data:/data
|
||||||
|
image: asklyphe/frontend
|
||||||
|
# vorebot:
|
||||||
|
# restart: unless-stopped
|
||||||
|
# networks:
|
||||||
|
# - lyphenet
|
||||||
|
# - outer
|
||||||
|
# env_file: ".env"
|
||||||
|
# depends_on:
|
||||||
|
# - nats
|
||||||
|
# build:
|
||||||
|
# dockerfile: Dockerfile.vorebot
|
||||||
|
# image: asklyphe/vorebot
|
||||||
|
|
||||||
|
# bingservice:
|
||||||
|
# restart: unless-stopped
|
||||||
|
# networks:
|
||||||
|
# - lyphenet
|
||||||
|
# - outer
|
||||||
|
# env_file: ".env"
|
||||||
|
# depends_on:
|
||||||
|
# - nats
|
||||||
|
# build:
|
||||||
|
# dockerfile: Dockerfile.bingservice
|
||||||
|
# volumes:
|
||||||
|
# - ./proxies.txt:/data/proxies.txt
|
||||||
|
# image: asklyphe/bingservice
|
||||||
|
# googleservice:
|
||||||
|
# restart: unless-stopped
|
||||||
|
# networks:
|
||||||
|
# - lyphenet
|
||||||
|
# - outer
|
||||||
|
# env_file: ".env"
|
||||||
|
# depends_on:
|
||||||
|
# - nats
|
||||||
|
# - bingservice
|
||||||
|
# build:
|
||||||
|
# dockerfile: Dockerfile.googleservice
|
||||||
|
# image: asklyphe/googleservice
|
||||||
|
|
||||||
|
nats:
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- lyphenet
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
env_file: ".env"
|
||||||
|
image: nats:2.11.8
|
||||||
|
command: "-js"
|
||||||
|
db:
|
||||||
|
image: postgres:17
|
||||||
|
networks:
|
||||||
|
- lyphenet
|
||||||
|
env_file: ".env"
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'pg_isready', '-U', 'postgres']
|
||||||
|
volumes:
|
||||||
|
- ./database:/var/lib/postgresql/data
|
||||||
|
proxy:
|
||||||
|
image: nginx:latest
|
||||||
|
networks:
|
||||||
|
- lyphenet
|
||||||
|
- outer
|
||||||
|
depends_on:
|
||||||
|
- frontend
|
||||||
|
- auth-frontend
|
||||||
|
volumes:
|
||||||
|
- ./nginx:/etc/nginx/conf.d
|
||||||
|
- frontend_data:/data/frontend
|
||||||
|
- auth_frontend_data:/data/auth-frontend
|
||||||
|
ports:
|
||||||
|
- "1234:80"
|
||||||
|
- "1235:81"
|
||||||
|
# caddy:
|
||||||
|
# image: caddy:latest
|
||||||
|
# networks:
|
||||||
|
# - lyphenet
|
||||||
|
# - outer
|
||||||
|
# depends_on:
|
||||||
|
# - frontend
|
||||||
|
# - auth-frontend
|
||||||
|
# volumes:
|
||||||
|
# - ./Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
# ports:
|
||||||
|
# - 1234:1234
|
||||||
|
# - 1235:1235
|
||||||
|
|
||||||
|
networks:
|
||||||
|
outer:
|
||||||
|
lyphenet:
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
frontend_data:
|
||||||
|
auth_frontend_data:
|
52
key.pem
Normal file
52
key.pem
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC28URbBWcTpvOa
|
||||||
|
r679u4CwsAHQ+i+9iPBvjRG/ShdvXkAgWm+t/BvKi9JGFOAn49IKOpcteY15qvDF
|
||||||
|
RPDKk8YWoiwdMQSKRNEwEow3YlIs6xX94+PNdwsjaqy/mhJTMh0xrElZJ5+B4mDX
|
||||||
|
QHOzdS6fe0SlNhqEAkFaIuUNX1NAks7yRnkC5LGkSHHjgD2ZThwyZ+cstvT7WEUN
|
||||||
|
9uMz/FfLuQQLrVZDydE9tsoQo0CIl1l0NLiE0BN5RIwii6Gkao74jlxh6tXv7XcO
|
||||||
|
TxZ1aV3F92qMKN1NtWFEqpC2PDdfLG5iAlwamKguD24NRMDC9CGCiutE4lRhRQWk
|
||||||
|
C89NSxOkG25MGRvK0jut7MiKOia/Xk5uJI2Rid9IWFKvxnuT5AW9PCbjM0OSkw2G
|
||||||
|
0PzthUAO1jrOyA2R50oh/YGsdslELhlpZSiu/StSx+0Ux0E9dcQHvnlllU5BrYXb
|
||||||
|
DkoSCiejhD4xV35KmhIwtz7pr2cajfeJ5r7Em/hSBVbSZqbv5OmGgxTSSDLUTaLJ
|
||||||
|
A015vLnLNCV5al/iGzXKl1FOwTIzRLv+og/jK70rwOGXRed2JnKntqfBEnR51gky
|
||||||
|
9axyyz3dAMEE1rCc+oOv7ycZoEKwPdXiyneOCLT40QT6No1UrMJCa32a4+YJgbAN
|
||||||
|
B8igFwwhdapD5N+qvpCWtiKsdnbPeQIDAQABAoICAALHmEMkqAWbK66QB7UhSe48
|
||||||
|
z7l0ImIQrrb1mB59EjHJOD6EiUJfc/2D4tvqYECsgqW3G30saifxWsLjxcpiuTA2
|
||||||
|
nhngUGAQD1IZ2pQwpyPkNyQC0b8qaquotZDPhekOmkrJ+Y0AF3D5Mk3+LVPfznz/
|
||||||
|
F8XOQ0uTwal7ZfNlyAveNqaYcktcV+hxkQSAfnnHTBpITGCab6in0Rxj24fyCCms
|
||||||
|
n5zleaEgOAyIUlPVSh1ZMeaT5eT4/BBdH8mAyXcuqRtMScmgOPMc1Q6m84xblxPA
|
||||||
|
JuTHEBwGivPK3Gbvpw7/ftiaSb41gsJnvPr0qjHeQ9jQhLdkk9iKth82oZc18kVg
|
||||||
|
ipF1TdSHz1EauoczyHM27aN1ZdPibkaic8QdPya0086sn+uXDRPELivV1xSMSHsH
|
||||||
|
xpEuANeL874X5h9Cpv4vWcJnQrbs81C4cXI46Mrc561uKljDVtvYFXvpdZuJ4GNp
|
||||||
|
C9zNNLp1ssmME9uLjLYIbmek/shb9XMpn7XhV0poWUZPGijI7qGLe6OoOqXdKoes
|
||||||
|
KMXkVJ5omfd/GvvmisJQaFcstqPO54MscFm4cBXQ0U+DxGT0QtSNi4YHtNs8EMdw
|
||||||
|
2AYlLN/DIzIm+YeR+rWNf8TYZbS1CazQj/ee4DTPAprKsumaR8/Cw+ACl6ICpUFA
|
||||||
|
bHMCd65TcV94F+LU7L5VAoIBAQDv8oPAUMeaFpY524h9k33HlNmvrmA5aF9tbeEp
|
||||||
|
b0QJpisldK8w8Rx9/7PS3NP5DO2z1GbNW8+iZE+QH03fIa1QeNBWISnfK8VUkUHn
|
||||||
|
8j6Q52O0bxC2v+a76U0MdP4JqfrihuHXvjH++9FN/KVwpAJlj7Fq8HugymEpi8+Y
|
||||||
|
Xv4VnzSm/sdbThvbSHzSGo8Y38dbN7pM6tOen36mxcAnOlH6GnTFEWYmo/f5jj8b
|
||||||
|
I/+rI8pmeDK6HPZFXw8FonEykX2My8OrN4iGLkFqlFfdgXHtuuPDLImxOCiJN0y7
|
||||||
|
bizq412/kh7Fbg7Q3oSULd9tmojVi3em4QWvxlxbOwIXjyT1AoIBAQDDLnOsvy2G
|
||||||
|
ajFvKJ3bSrh3OQdGFyJn0nquZtgzDXsatrItaBYZtUIqnRsfjQZqyvoYNRDqs9QR
|
||||||
|
xmqB7guPkqLN/mk+mAjmHWPs0oP0Y9S2IXl5/CRIuM3ZcVTVVsPC8lOrXAH3X1jZ
|
||||||
|
OJiUG/fUUJoonDPnFZLGajefmFsUWjyIr85VOUMhYsq2lA3ZTyM6rQLX3gM//36u
|
||||||
|
d70K1kXPWoWIsbaztPpqBSJK05EjztVmkUYbPKqHVz+8TD4Xr1baSC1Q0KuHqrr1
|
||||||
|
451biNN/TSG5GOgdJRZcVXh0imp+xQjB3x2UmwNKk4uRXRWnoa0QlhKm0kbapaGP
|
||||||
|
QVCUgwlQOA31AoIBAGmvhbx1WBVUkYKWYX3+Ms5vj5pD0fo3MKEAXsZjTbJ6UFLF
|
||||||
|
HE0QRh5xPAFKZssxmJk2mrklEUVTrX+rah83tCDXtdvZ65lyrA3dlQvWtRwZ7t6Q
|
||||||
|
dOopiDWIQvmTpjkXd3vDMUJXcan/vGb/OtdsRen56olRtwJRYY5tGFjirkNTxlsv
|
||||||
|
qRtcQgTJ3sCkFhc8qZBR8Wrjm6YoVh6ax1H/7A+fC4OpcDbgzd5Lexw3NOtqbkHH
|
||||||
|
+3/iNc7EWdd/fyBo2MXlEiAd67I+OW36POFBnK67PIrA2T0HoUMe6ls74ejrkGVK
|
||||||
|
tOb83OW+vOKPefPKty5nqaIFRv3u/sroKLm7wOkCggEAFBsR4WakKud/hiLZ9///
|
||||||
|
dpCSVj8F1UoSRyri9Idb+gl92z2QoT9RvJAIfjyJv7B/CMVWo8a4fshAqne6CyUg
|
||||||
|
zjV54+/HYuT+KSQaYa9y9vwFxnIZzr/yvIZ3Ja7VZZyOz+UfcrsIrP+uf/tNkTpo
|
||||||
|
VuyYUCKhxvykFDWelD8jYzUw/Qh0CNljZmFj99G2IFI4K8J79Ti9dP1ypM4jzNNX
|
||||||
|
VBhyaJqo/QjgWnLmzZh91R376cxbCKwNLblw4AG44a1ztZJ5SPVmYvP6frZeiwuI
|
||||||
|
AMg3COGMJyDK0r57b+meGFKCeo9pTGJcizHajDUUXdQHwdWBZP6Q4O/qfBHvgKr1
|
||||||
|
jQKCAQEA4UaBpHUcO9TG5NEMX+IOMurcuYnlf+12ImGix21A2ylOI7RIfA/RCfgK
|
||||||
|
7UAwT9OqTNhvgJ9DCth7y6TJ6/4UnKJ1duaM7sP6vX3Vq8z0zixYDZJ2bE4COgBJ
|
||||||
|
tzAOLM22cpIJj98XhCdMOzkVse7vQDpBJPmTh19pqfuFnGLmsIIdLuo64Xa+6bvB
|
||||||
|
p21edHgxH1pl82rfnvMTMzoicH0mQZQD+l19O6togwhpJY1YbPaqGlCavlQqMKyC
|
||||||
|
r8fseEBic1de7Y9XaG25TS+lZVg53vOB2BNZM6Z8CjeRf3rdZZd+cCqa7aFsdmER
|
||||||
|
hfHYKzHGaDbp/aPWH8HQzfs6QxGRog==
|
||||||
|
-----END PRIVATE KEY-----
|
30
lyphedb/.gitignore
vendored
Normal file
30
lyphedb/.gitignore
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# ---> Rust
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# RustRover
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
|
||||||
|
# Added by cargo
|
||||||
|
|
||||||
|
/target
|
||||||
|
|
||||||
|
# don't commit employee test configs (:
|
||||||
|
husky_config.toml
|
18
lyphedb/Cargo.toml
Normal file
18
lyphedb/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "lyphedb"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
log = "0.4.20"
|
||||||
|
rmp-serde = "1.1.2"
|
||||||
|
futures = "0.3.30"
|
||||||
|
async-nats = "0.38.0"
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
chrono = "0.4.31"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
rand = "0.9.0"
|
||||||
|
toml = "0.8.20"
|
||||||
|
env_logger = "0.11.6"
|
||||||
|
once_cell = "1.20.3"
|
||||||
|
percent-encoding = "2.3.1"
|
10
lyphedb/config.toml
Normal file
10
lyphedb/config.toml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
name = "lyphedb-test"
|
||||||
|
write = true
|
||||||
|
master = "/lyphedb_master"
|
||||||
|
ram_limit = "1mb" # supported suffixes: b,kb,mb,gb
|
||||||
|
gc_limit = "512kb"
|
||||||
|
avg_entry_size = "1kb"
|
||||||
|
log = "debug"
|
||||||
|
nats_cert = "nats/nats.cert"
|
||||||
|
nats_key = "nats/nats.pem"
|
||||||
|
nats_url = "127.0.0.1:4222"
|
14
lyphedb/ldbtesttool/Cargo.toml
Normal file
14
lyphedb/ldbtesttool/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "ldbtesttool"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rmp-serde = "1.1.2"
|
||||||
|
futures = "0.3.30"
|
||||||
|
async-nats = "0.38.0"
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
lyphedb = { path = "../" }
|
||||||
|
toml = "0.8.20"
|
||||||
|
rand = "0.9.0"
|
40
lyphedb/ldbtesttool/src/config.rs
Normal file
40
lyphedb/ldbtesttool/src/config.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ConfigFile {
|
||||||
|
pub name: String,
|
||||||
|
pub nats_cert: String,
|
||||||
|
pub nats_key: String,
|
||||||
|
pub nats_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LypheDBConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub nats_cert: String,
|
||||||
|
pub nats_key: String,
|
||||||
|
pub nats_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config(path: &str) -> LypheDBConfig {
|
||||||
|
let s = std::fs::read_to_string(path);
|
||||||
|
if let Err(e) = s {
|
||||||
|
panic!("failed to read config file: {}", e);
|
||||||
|
}
|
||||||
|
let s = s.unwrap();
|
||||||
|
let cnf = toml::from_str::<ConfigFile>(&s);
|
||||||
|
if let Err(e) = cnf {
|
||||||
|
panic!("failed to parse config file: {}", e);
|
||||||
|
}
|
||||||
|
let cnf = cnf.unwrap();
|
||||||
|
|
||||||
|
// quick checks and conversions
|
||||||
|
let mut config = LypheDBConfig {
|
||||||
|
name: cnf.name,
|
||||||
|
nats_cert: cnf.nats_cert,
|
||||||
|
nats_key: cnf.nats_key,
|
||||||
|
nats_url: cnf.nats_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
114
lyphedb/ldbtesttool/src/main.rs
Normal file
114
lyphedb/ldbtesttool/src/main.rs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
use crate::config::load_config;
|
||||||
|
use lyphedb::{KVList, KeyList, LDBNatsMessage, LypheDBCommand, PropagationStrategy};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use async_nats::Message;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let config = load_config(&std::env::args().nth(1).expect("please specify config file"));
|
||||||
|
let nats = async_nats::ConnectOptions::new()
|
||||||
|
.add_client_certificate(
|
||||||
|
config.nats_cert.as_str().into(),
|
||||||
|
config.nats_key.as_str().into(),
|
||||||
|
)
|
||||||
|
.connect(config.nats_url.as_str())
|
||||||
|
.await;
|
||||||
|
if let Err(e) = nats {
|
||||||
|
eprintln!("FATAL ERROR, COULDN'T CONNECT TO NATS: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let nats = nats.unwrap();
|
||||||
|
|
||||||
|
// test 1: create 10000 keys, send in chunks of 10
|
||||||
|
println!("test 1: create 10_000 keys, send in chunks of 100");
|
||||||
|
let (key_tx, mut key_rx) = mpsc::unbounded_channel();
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let mut tasks = vec![];
|
||||||
|
for _ in 0..(10_000 / 100) {
|
||||||
|
let name = config.name.clone();
|
||||||
|
let nats = nats.clone();
|
||||||
|
let key_tx = key_tx.clone();
|
||||||
|
tasks.push(tokio::spawn(async move {
|
||||||
|
let mut keys = BTreeMap::new();
|
||||||
|
for _ in 0..100 {
|
||||||
|
let key = rand::random::<[u8; 16]>();
|
||||||
|
let data = rand::random::<[u8; 16]>();
|
||||||
|
key_tx.send((key, data)).unwrap();
|
||||||
|
keys.insert(key, data);
|
||||||
|
}
|
||||||
|
let data = rmp_serde::to_vec(&LDBNatsMessage::Command(LypheDBCommand::SetKeys(
|
||||||
|
KVList {
|
||||||
|
kvs: keys
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k.to_vec(), v.to_vec()))
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
PropagationStrategy::Immediate,
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
let replyto_sub = "lyphedb_test_1".to_string();
|
||||||
|
let mut subscriber = nats.subscribe(replyto_sub.clone()).await.unwrap();
|
||||||
|
nats.publish_with_reply(name.clone(), replyto_sub, data.into()).await.unwrap();
|
||||||
|
if let Some(reply) = subscriber.next().await {
|
||||||
|
let reply = rmp_serde::from_slice::<LDBNatsMessage>(&reply.payload).unwrap();
|
||||||
|
if let LDBNatsMessage::Success = reply {} else {
|
||||||
|
eprintln!("failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for task in tasks {
|
||||||
|
task.await.unwrap();
|
||||||
|
}
|
||||||
|
let end = std::time::Instant::now();
|
||||||
|
println!("test 1: {}ms", end.duration_since(start).as_millis());
|
||||||
|
let mut our_copy = BTreeMap::new();
|
||||||
|
let mut buffer = vec![];
|
||||||
|
key_rx.recv_many(&mut buffer, 10_000).await;
|
||||||
|
for (k, v) in buffer {
|
||||||
|
our_copy.insert(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// test 2: read back all keys and check for accuracy
|
||||||
|
println!("test 2: read back all keys and check for accuracy");
|
||||||
|
let kv: Vec<_> = our_copy.into_iter().map(|(k, v)| (k.to_vec(), v.to_vec())).collect();
|
||||||
|
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let mut tasks = vec![];
|
||||||
|
for i in 0..100 {
|
||||||
|
let batch = kv[i * 100..((i + 1) * 100).min(kv.len())].to_vec();
|
||||||
|
let name = config.name.clone();
|
||||||
|
let nats = nats.clone();
|
||||||
|
tasks.push(tokio::spawn(async move {
|
||||||
|
let data = rmp_serde::to_vec(&LDBNatsMessage::Command(LypheDBCommand::GetKeys(
|
||||||
|
KeyList {
|
||||||
|
keys: batch.iter().map(|(k, _)| k.to_vec()).collect()
|
||||||
|
}
|
||||||
|
))).unwrap();
|
||||||
|
let replyto_sub = format!("lyphedb_test_2_{}", i);
|
||||||
|
let mut subscriber = nats.subscribe(replyto_sub.clone()).await.unwrap();
|
||||||
|
nats.publish_with_reply(name.clone(), replyto_sub, data.into()).await.unwrap();
|
||||||
|
if let Some(reply) = subscriber.next().await {
|
||||||
|
let reply = rmp_serde::from_slice::<LDBNatsMessage>(&reply.payload).unwrap();
|
||||||
|
if let LDBNatsMessage::Entries(kvlist) = reply {
|
||||||
|
// check all keys
|
||||||
|
for (k1, v1) in batch {
|
||||||
|
let v2 = kvlist.kvs.iter().find(|(k, v)| k == &k1).expect("key not found");
|
||||||
|
assert_eq!(v1, v2.1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for task in tasks {
|
||||||
|
task.await.unwrap();
|
||||||
|
}
|
||||||
|
let end = std::time::Instant::now();
|
||||||
|
println!("test 2: {}ms", end.duration_since(start).as_millis());
|
||||||
|
}
|
129
lyphedb/src/config.rs
Normal file
129
lyphedb/src/config.rs
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ConfigFile {
|
||||||
|
pub name: String,
|
||||||
|
pub write: bool,
|
||||||
|
pub master: String,
|
||||||
|
pub ram_limit: String,
|
||||||
|
pub gc_limit: String,
|
||||||
|
pub avg_entry_size: String,
|
||||||
|
pub log: String,
|
||||||
|
pub nats_cert: String,
|
||||||
|
pub nats_key: String,
|
||||||
|
pub nats_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LypheDBConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub write: bool,
|
||||||
|
pub master: String,
|
||||||
|
pub ram_limit: usize,
|
||||||
|
pub gc_limit: usize,
|
||||||
|
pub avg_entry_size: usize,
|
||||||
|
pub log: String,
|
||||||
|
pub nats_cert: String,
|
||||||
|
pub nats_key: String,
|
||||||
|
pub nats_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config(path: &str) -> LypheDBConfig {
|
||||||
|
let s = std::fs::read_to_string(path);
|
||||||
|
if let Err(e) = s {
|
||||||
|
panic!("failed to read config file: {}", e);
|
||||||
|
}
|
||||||
|
let s = s.unwrap();
|
||||||
|
let cnf = toml::from_str::<ConfigFile>(&s);
|
||||||
|
if let Err(e) = cnf {
|
||||||
|
panic!("failed to parse config file: {}", e);
|
||||||
|
}
|
||||||
|
let cnf = cnf.unwrap();
|
||||||
|
|
||||||
|
// quick checks and conversions
|
||||||
|
let mut config = LypheDBConfig {
|
||||||
|
name: cnf.name,
|
||||||
|
write: cnf.write,
|
||||||
|
master: cnf.master,
|
||||||
|
ram_limit: 0,
|
||||||
|
gc_limit: 0,
|
||||||
|
avg_entry_size: 0,
|
||||||
|
log: cnf.log,
|
||||||
|
nats_cert: cnf.nats_cert,
|
||||||
|
nats_key: cnf.nats_key,
|
||||||
|
nats_url: cnf.nats_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(cnf.ram_limit.ends_with("b") &&
|
||||||
|
(
|
||||||
|
cnf.ram_limit.trim_end_matches("b").ends_with("k") ||
|
||||||
|
cnf.ram_limit.trim_end_matches("b").ends_with("m") ||
|
||||||
|
cnf.ram_limit.trim_end_matches("b").ends_with("g")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
panic!("invalid ram limit");
|
||||||
|
}
|
||||||
|
config.ram_limit = if cnf.ram_limit.ends_with("gb") {
|
||||||
|
cnf.ram_limit.trim_end_matches("gb").parse::<usize>().unwrap() * 1024 * 1024 * 1024
|
||||||
|
} else if cnf.ram_limit.ends_with("mb") {
|
||||||
|
cnf.ram_limit.trim_end_matches("mb").parse::<usize>().unwrap() * 1024 * 1024
|
||||||
|
} else if cnf.ram_limit.ends_with("kb") {
|
||||||
|
cnf.ram_limit.trim_end_matches("kb").parse::<usize>().unwrap() * 1024
|
||||||
|
} else {
|
||||||
|
cnf.ram_limit.trim_end_matches("b").parse::<usize>().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(cnf.gc_limit.ends_with("b") &&
|
||||||
|
(
|
||||||
|
cnf.gc_limit.trim_end_matches("b").ends_with("k") ||
|
||||||
|
cnf.gc_limit.trim_end_matches("b").ends_with("m") ||
|
||||||
|
cnf.gc_limit.trim_end_matches("b").ends_with("g")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
panic!("invalid ram limit");
|
||||||
|
}
|
||||||
|
config.gc_limit = if cnf.gc_limit.ends_with("gb") {
|
||||||
|
cnf.gc_limit.trim_end_matches("gb").parse::<usize>().unwrap() * 1024 * 1024 * 1024
|
||||||
|
} else if cnf.gc_limit.ends_with("mb") {
|
||||||
|
cnf.gc_limit.trim_end_matches("mb").parse::<usize>().unwrap() * 1024 * 1024
|
||||||
|
} else if cnf.gc_limit.ends_with("kb") {
|
||||||
|
cnf.gc_limit.trim_end_matches("kb").parse::<usize>().unwrap() * 1024
|
||||||
|
} else {
|
||||||
|
cnf.gc_limit.trim_end_matches("b").parse::<usize>().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(cnf.avg_entry_size.ends_with("b") &&
|
||||||
|
(
|
||||||
|
cnf.avg_entry_size.trim_end_matches("b").ends_with("k") ||
|
||||||
|
cnf.avg_entry_size.trim_end_matches("b").ends_with("m") ||
|
||||||
|
cnf.avg_entry_size.trim_end_matches("b").ends_with("g")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
panic!("invalid ram limit");
|
||||||
|
}
|
||||||
|
config.avg_entry_size = if cnf.avg_entry_size.ends_with("gb") {
|
||||||
|
cnf.avg_entry_size.trim_end_matches("gb").parse::<usize>().unwrap() * 1024 * 1024 * 1024
|
||||||
|
} else if cnf.avg_entry_size.ends_with("mb") {
|
||||||
|
cnf.avg_entry_size.trim_end_matches("mb").parse::<usize>().unwrap() * 1024 * 1024
|
||||||
|
} else if cnf.avg_entry_size.ends_with("kb") {
|
||||||
|
cnf.avg_entry_size.trim_end_matches("kb").parse::<usize>().unwrap() * 1024
|
||||||
|
} else {
|
||||||
|
cnf.avg_entry_size.trim_end_matches("b").parse::<usize>().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
if config.avg_entry_size > config.ram_limit {
|
||||||
|
panic!("avg entry size is larger than ram limit");
|
||||||
|
}
|
||||||
|
if config.gc_limit > config.ram_limit {
|
||||||
|
panic!("gc limit is larger than ram limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.log.is_empty() {
|
||||||
|
config.log = "info".to_string();
|
||||||
|
}
|
||||||
|
if config.log != "debug" && config.log != "info" && config.log != "warn" && config.log != "error" {
|
||||||
|
panic!("invalid log level");
|
||||||
|
}
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
321
lyphedb/src/dbimpl/mod.rs
Normal file
321
lyphedb/src/dbimpl/mod.rs
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
use crate::config::LypheDBConfig;
|
||||||
|
use log::*;
|
||||||
|
use lyphedb::PropagationStrategy;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
pub const NOT_ALLOWED_ASCII: AsciiSet = CONTROLS.add(b' ').add(b'/').add(b'.').add(b'\\');
|
||||||
|
|
||||||
|
pub type LDBCache = BTreeMap<Arc<Vec<u8>>, Arc<Vec<u8>>>;
|
||||||
|
|
||||||
|
/// Reads will read from this cache
|
||||||
|
pub static PRIMARY_CACHE: Lazy<RwLock<LDBCache>> =
|
||||||
|
Lazy::new(|| RwLock::new(BTreeMap::new()));
|
||||||
|
/// Writes will be written to this cache, and then depending on the propagation method, the
|
||||||
|
/// Primary cache will be set to a copy
|
||||||
|
pub static SECONDARY_CACHE: Lazy<RwLock<LDBCache>> =
|
||||||
|
Lazy::new(|| RwLock::new(BTreeMap::new()));
|
||||||
|
/// how often are keys accessed? this will influence the garbage collector
|
||||||
|
pub static KEY_ACCESS_COUNTER: Lazy<RwLock<BTreeMap<Arc<Vec<u8>>, AtomicU64>>> =
|
||||||
|
Lazy::new(|| RwLock::new(BTreeMap::new()));
|
||||||
|
|
||||||
|
pub fn key_to_path(config: &LypheDBConfig, key: &[u8]) -> PathBuf {
|
||||||
|
let mut path = PathBuf::new();
|
||||||
|
path.push(&config.master);
|
||||||
|
|
||||||
|
let mut fnbuf = Vec::new();
|
||||||
|
for (i, byte) in key.iter().enumerate() {
|
||||||
|
if *byte == b'/' {
|
||||||
|
let encoded = percent_encode(&fnbuf, &NOT_ALLOWED_ASCII).to_string();
|
||||||
|
path.push(encoded);
|
||||||
|
fnbuf.clear();
|
||||||
|
} else {
|
||||||
|
fnbuf.push(*byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !fnbuf.is_empty() {
|
||||||
|
let encoded = percent_encode(&fnbuf, &NOT_ALLOWED_ASCII).to_string();
|
||||||
|
path.push(encoded);
|
||||||
|
fnbuf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
path.push(".self");
|
||||||
|
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OperationError {
|
||||||
|
KeyCannotBeEmpty,
|
||||||
|
FilesystemPermissionError,
|
||||||
|
BadFilesystemEntry,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_key_disk(
|
||||||
|
config: &LypheDBConfig,
|
||||||
|
key: &[u8],
|
||||||
|
value: &[u8],
|
||||||
|
) -> Result<(), OperationError> {
|
||||||
|
if key.is_empty() {
|
||||||
|
return Err(OperationError::KeyCannotBeEmpty);
|
||||||
|
}
|
||||||
|
let path = key_to_path(config, key);
|
||||||
|
let directory = path.parent().ok_or(OperationError::KeyCannotBeEmpty)?;
|
||||||
|
if let Ok(directory_exists) = directory.try_exists() {
|
||||||
|
if !directory_exists {
|
||||||
|
std::fs::create_dir_all(directory).map_err(|e| {
|
||||||
|
warn!("couldn't create directory: {:?}", e);
|
||||||
|
OperationError::FilesystemPermissionError
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(OperationError::FilesystemPermissionError);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::write(path, value).map_err(|e| {
|
||||||
|
warn!("couldn't write file: {:?}", e);
|
||||||
|
OperationError::FilesystemPermissionError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_key_disk(
|
||||||
|
config: &LypheDBConfig,
|
||||||
|
key: &[u8],
|
||||||
|
) -> Result<(), OperationError> {
|
||||||
|
if key.is_empty() {
|
||||||
|
return Err(OperationError::KeyCannotBeEmpty);
|
||||||
|
}
|
||||||
|
let path = key_to_path(config, key);
|
||||||
|
if let Ok(exists) = path.try_exists() {
|
||||||
|
if !exists {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fs::remove_file(path).map_err(|e| {
|
||||||
|
warn!("couldn't remove file: {:?}", e);
|
||||||
|
OperationError::FilesystemPermissionError
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_key_disk(
|
||||||
|
config: &LypheDBConfig,
|
||||||
|
key: &[u8],
|
||||||
|
) -> Result<Option<Vec<u8>>, OperationError> {
|
||||||
|
if key.is_empty() {
|
||||||
|
return Err(OperationError::KeyCannotBeEmpty);
|
||||||
|
}
|
||||||
|
let path = key_to_path(config, key);
|
||||||
|
if let Ok(exists) = path.try_exists() {
|
||||||
|
if !exists {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let data = std::fs::read(path).map_err(|e| {
|
||||||
|
warn!("couldn't read file: {:?}", e);
|
||||||
|
OperationError::FilesystemPermissionError
|
||||||
|
})?;
|
||||||
|
Ok(Some(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// this function allows empty keys, so you can get all keys under root by doing
|
||||||
|
/// get_keys_under_keydir_disk(..., b"")
|
||||||
|
pub async fn get_keys_under_keydir_disk(
|
||||||
|
config: &LypheDBConfig,
|
||||||
|
keydir: &[u8],
|
||||||
|
) -> Result<Vec<Vec<u8>>, OperationError> {
|
||||||
|
let path = key_to_path(config, keydir);
|
||||||
|
let path = path.parent().expect("bad master");
|
||||||
|
let mut keys = Vec::new();
|
||||||
|
for entry in std::fs::read_dir(path).map_err(|e| {
|
||||||
|
warn!("couldn't read directory: {:?}", e);
|
||||||
|
OperationError::FilesystemPermissionError
|
||||||
|
})? {
|
||||||
|
let entry = entry.map_err(|e| {
|
||||||
|
warn!("couldn't read directory entry: {:?}", e);
|
||||||
|
OperationError::FilesystemPermissionError
|
||||||
|
})?;
|
||||||
|
let path = entry.path();
|
||||||
|
let filename = path
|
||||||
|
.to_str()
|
||||||
|
.ok_or(OperationError::FilesystemPermissionError)?;
|
||||||
|
if filename.ends_with(".self") {
|
||||||
|
// this is a value file, ignore
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let filename = filename.trim_start_matches(&config.master);
|
||||||
|
let key = percent_decode_str(filename).collect();
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_key(
|
||||||
|
config: LypheDBConfig,
|
||||||
|
key: &[u8],
|
||||||
|
value: &[u8],
|
||||||
|
strat: &PropagationStrategy,
|
||||||
|
) -> Result<(), OperationError> {
|
||||||
|
let k1 = Arc::new(key.to_vec());
|
||||||
|
let v1 = Arc::new(value.to_vec());
|
||||||
|
let disk_task = {
|
||||||
|
let k1 = k1.clone();
|
||||||
|
let v1 = v1.clone();
|
||||||
|
tokio::spawn(async move { set_key_disk(&config, &k1, &v1).await })
|
||||||
|
};
|
||||||
|
|
||||||
|
let prop_task = match strat {
|
||||||
|
PropagationStrategy::Immediate => {
|
||||||
|
let k1 = k1.clone();
|
||||||
|
let v1 = v1.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut secondary_cache = SECONDARY_CACHE.write().await;
|
||||||
|
secondary_cache.insert(k1, v1);
|
||||||
|
let mut primary_cache = PRIMARY_CACHE.write().await;
|
||||||
|
*primary_cache = secondary_cache.clone();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
PropagationStrategy::Timeout => {
|
||||||
|
let k1 = k1.clone();
|
||||||
|
let v1 = v1.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
{
|
||||||
|
let mut secondary_cache = SECONDARY_CACHE.write().await;
|
||||||
|
secondary_cache.insert(k1, v1);
|
||||||
|
}
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
loop {
|
||||||
|
if start.elapsed().as_secs() > 60 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let pc = PRIMARY_CACHE.try_write();
|
||||||
|
if pc.is_err() {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut primary_cache = PRIMARY_CACHE.write().await;
|
||||||
|
let secondary_cache = SECONDARY_CACHE.read().await;
|
||||||
|
*primary_cache = secondary_cache.clone();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
PropagationStrategy::OnRead => {
|
||||||
|
let k1 = k1.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
{
|
||||||
|
let mut secondary_cache = SECONDARY_CACHE.write().await;
|
||||||
|
secondary_cache.remove(&k1);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut primary_cache = PRIMARY_CACHE.write().await;
|
||||||
|
primary_cache.remove(&k1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(Err(e)) = disk_task.await {
|
||||||
|
error!("couldn't set key on disk: {:?} ({:?})", e, key);
|
||||||
|
// undo propagation
|
||||||
|
prop_task.abort();
|
||||||
|
{
|
||||||
|
let mut primary_cache = PRIMARY_CACHE.write().await;
|
||||||
|
primary_cache.remove(&k1);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut secondary_cache = SECONDARY_CACHE.write().await;
|
||||||
|
secondary_cache.remove(&k1);
|
||||||
|
}
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
let _ = prop_task.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_key(config: LypheDBConfig, key: &[u8]) -> Result<Option<Vec<u8>>, OperationError> {
|
||||||
|
let k1 = Arc::new(key.to_vec());
|
||||||
|
{
|
||||||
|
let k1 = k1.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut access_counter = KEY_ACCESS_COUNTER.write().await;
|
||||||
|
let counter = access_counter.entry(k1.clone()).or_insert(AtomicU64::new(0));
|
||||||
|
if counter.load(Ordering::Relaxed) > 10000000 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
counter.fetch_add(1, Ordering::SeqCst);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let disk_task = {
|
||||||
|
let k1 = k1.clone();
|
||||||
|
tokio::spawn(async move { get_key_disk(&config, &k1).await })
|
||||||
|
};
|
||||||
|
{
|
||||||
|
// read from cache
|
||||||
|
let cache = PRIMARY_CACHE.read().await;
|
||||||
|
if let Some(val) = cache.get(&k1) {
|
||||||
|
disk_task.abort();
|
||||||
|
return Ok(Some(val.to_vec()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//debug!("cache miss");
|
||||||
|
if let Ok(result) = disk_task.await {
|
||||||
|
if let Ok(Some(val)) = &result {
|
||||||
|
let val = Arc::new(val.to_vec());
|
||||||
|
tokio::spawn(async move {
|
||||||
|
{
|
||||||
|
let mut cache = SECONDARY_CACHE.write().await;
|
||||||
|
cache.insert(k1.clone(), val.clone());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let secondary_cache = SECONDARY_CACHE.read().await;
|
||||||
|
let mut primary_cache = PRIMARY_CACHE.write().await;
|
||||||
|
*primary_cache = secondary_cache.clone();
|
||||||
|
}
|
||||||
|
//debug!("cache insert");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
Err(OperationError::FilesystemPermissionError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_key(config: LypheDBConfig, key: &[u8]) -> Result<(), OperationError> {
|
||||||
|
let k1 = Arc::new(key.to_vec());
|
||||||
|
let disk_task = {
|
||||||
|
let k1 = k1.clone();
|
||||||
|
tokio::spawn(async move { delete_key_disk(&config, &k1).await })
|
||||||
|
};
|
||||||
|
let prop_task = {
|
||||||
|
let k1 = k1.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
{
|
||||||
|
let mut key_access_counter = KEY_ACCESS_COUNTER.write().await;
|
||||||
|
key_access_counter.remove(&k1);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut secondary_cache = SECONDARY_CACHE.write().await;
|
||||||
|
secondary_cache.remove(&k1);
|
||||||
|
}
|
||||||
|
let mut primary_cache = PRIMARY_CACHE.write().await;
|
||||||
|
let secondary_cache = SECONDARY_CACHE.read().await;
|
||||||
|
*primary_cache = secondary_cache.clone();
|
||||||
|
})
|
||||||
|
};
|
||||||
|
prop_task.await.expect("couldn't delete key");
|
||||||
|
disk_task.await.expect("couldn't delete key")
|
||||||
|
}
|
49
lyphedb/src/lib.rs
Normal file
49
lyphedb/src/lib.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum LDBNatsMessage {
|
||||||
|
Command(LypheDBCommand),
|
||||||
|
Entries(KVList),
|
||||||
|
Count(u64),
|
||||||
|
Success,
|
||||||
|
BadRequest,
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum PropagationStrategy {
|
||||||
|
/// Reads will immediately be able to read this key's value as soon as it has been set
|
||||||
|
Immediate,
|
||||||
|
/// The value change will be queued along with others,
|
||||||
|
/// then in a period of either inactivity or a maximum time, all changes will be immediately
|
||||||
|
/// seen at once
|
||||||
|
Timeout,
|
||||||
|
/// The key will be removed from the cache, and thus the next read will cause a cache miss,
|
||||||
|
/// and the value will be loaded from disk
|
||||||
|
OnRead,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum LypheDBCommand {
|
||||||
|
SetKeys(KVList, PropagationStrategy),
|
||||||
|
GetKeys(KeyList),
|
||||||
|
CountKeys(KeyDirectory),
|
||||||
|
/// NOT RECURSIVE
|
||||||
|
GetKeyDirectory(KeyDirectory),
|
||||||
|
DeleteKeys(KeyList),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct KVList {
|
||||||
|
pub kvs: Vec<(Vec<u8>, Vec<u8>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct KeyList {
|
||||||
|
pub keys: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct KeyDirectory {
|
||||||
|
pub key: Vec<u8>,
|
||||||
|
}
|
248
lyphedb/src/main.rs
Normal file
248
lyphedb/src/main.rs
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
/*
|
||||||
|
keys are stored on disk like this
|
||||||
|
<master>/<keyname>/.self
|
||||||
|
|
||||||
|
so if i stored the key "/this/is/a/key" with the data "hello world", it'd look like this
|
||||||
|
|
||||||
|
<master>/this/is/a/key/.self -> "hello world"
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::config::{LypheDBConfig, load_config};
|
||||||
|
use crate::dbimpl::{KEY_ACCESS_COUNTER, PRIMARY_CACHE, SECONDARY_CACHE};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use log::{debug, error, info, warn};
|
||||||
|
use lyphedb::{KVList, LDBNatsMessage, LypheDBCommand};
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod dbimpl;
|
||||||
|
|
||||||
|
pub async fn gc_thread(config: LypheDBConfig) {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(61)).await;
|
||||||
|
{
|
||||||
|
let cache = PRIMARY_CACHE.read().await;
|
||||||
|
let cache_size = cache.len() * config.avg_entry_size;
|
||||||
|
if cache_size > config.gc_limit {
|
||||||
|
debug!("gc triggered, cache size: {} bytes", cache_size);
|
||||||
|
let keycount_to_remove = cache.len() - config.gc_limit / config.avg_entry_size;
|
||||||
|
drop(cache);
|
||||||
|
let mut least_freq_keys = vec![];
|
||||||
|
for (key, count) in KEY_ACCESS_COUNTER.read().await.iter() {
|
||||||
|
let count = count.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
if least_freq_keys.len() < keycount_to_remove {
|
||||||
|
least_freq_keys.push((key.clone(), count));
|
||||||
|
} else {
|
||||||
|
for (other, oc) in least_freq_keys.iter_mut() {
|
||||||
|
if count < *oc {
|
||||||
|
*other = key.clone();
|
||||||
|
*oc = count;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut cache = SECONDARY_CACHE.write().await;
|
||||||
|
for (key, _) in least_freq_keys.iter() {
|
||||||
|
cache.remove(key);
|
||||||
|
}
|
||||||
|
let mut primary_cache = PRIMARY_CACHE.write().await;
|
||||||
|
*primary_cache = cache.clone();
|
||||||
|
debug!(
|
||||||
|
"gc finished, cache size: {} bytes",
|
||||||
|
primary_cache.len() * config.avg_entry_size
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn precache_keys_until_limit(config: LypheDBConfig) {
|
||||||
|
info!("precache started");
|
||||||
|
let mut precache_count = 0;
|
||||||
|
let mut precache_stack = dbimpl::get_keys_under_keydir_disk(&config, b"")
|
||||||
|
.await
|
||||||
|
.expect("couldn't get root keys");
|
||||||
|
while let Some(precache_key) = precache_stack.pop() {
|
||||||
|
{
|
||||||
|
let cache = PRIMARY_CACHE.read().await;
|
||||||
|
if cache.len() * config.avg_entry_size > config.gc_limit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if cache.len() % 10000 == 0 {
|
||||||
|
info!("precache size: {} mb", (cache.len() * config.avg_entry_size) / (1024*1024));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let children = dbimpl::get_keys_under_keydir_disk(&config, &precache_key)
|
||||||
|
.await
|
||||||
|
.expect("couldn't get children of key");
|
||||||
|
if !children.is_empty() {
|
||||||
|
precache_stack.extend(children);
|
||||||
|
} else {
|
||||||
|
let _value = dbimpl::get_key(config.clone(), &precache_key)
|
||||||
|
.await
|
||||||
|
.expect("couldn't get value of key");
|
||||||
|
precache_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("precache finished, {} values precached", precache_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let config = load_config(&std::env::args().nth(1).expect("please specify config file"));
|
||||||
|
let logger = env_logger::builder()
|
||||||
|
.filter_level(match config.log.as_str() {
|
||||||
|
"debug" => log::LevelFilter::Debug,
|
||||||
|
"info" => log::LevelFilter::Info,
|
||||||
|
"warn" => log::LevelFilter::Warn,
|
||||||
|
"error" => log::LevelFilter::Error,
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
log::set_boxed_logger(Box::new(logger)).unwrap();
|
||||||
|
log::set_max_level(match config.log.as_str() {
|
||||||
|
"debug" => log::LevelFilter::Debug,
|
||||||
|
"info" => log::LevelFilter::Info,
|
||||||
|
"warn" => log::LevelFilter::Warn,
|
||||||
|
"error" => log::LevelFilter::Error,
|
||||||
|
_ => unreachable!(),
|
||||||
|
});
|
||||||
|
info!("lyphedb started");
|
||||||
|
|
||||||
|
let nats = async_nats::ConnectOptions::new()
|
||||||
|
.add_client_certificate(
|
||||||
|
config.nats_cert.as_str().into(),
|
||||||
|
config.nats_key.as_str().into(),
|
||||||
|
)
|
||||||
|
.connect(config.nats_url.as_str())
|
||||||
|
.await;
|
||||||
|
if let Err(e) = nats {
|
||||||
|
error!("FATAL ERROR, COULDN'T CONNECT TO NATS: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let nats = nats.unwrap();
|
||||||
|
|
||||||
|
let mut subscriber = nats
|
||||||
|
.queue_subscribe(config.name.clone(), "lyphedb".to_string())
|
||||||
|
.await
|
||||||
|
.expect("couldn't subscribe to subject");
|
||||||
|
|
||||||
|
info!("nats connected");
|
||||||
|
|
||||||
|
tokio::spawn(precache_keys_until_limit(config.clone()));
|
||||||
|
tokio::spawn(gc_thread(config.clone()));
|
||||||
|
|
||||||
|
while let Some(msg) = subscriber.next().await {
|
||||||
|
let config = config.clone();
|
||||||
|
let nats = nats.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
async fn bad_request(nats: &async_nats::Client, replyto: async_nats::Subject) {
|
||||||
|
let reply = rmp_serde::to_vec(&LDBNatsMessage::BadRequest).unwrap();
|
||||||
|
if let Err(e) = nats.publish(replyto, reply.into()).await {
|
||||||
|
warn!("couldn't send reply: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = msg.payload.to_vec();
|
||||||
|
let data = rmp_serde::from_slice::<LDBNatsMessage>(&data);
|
||||||
|
if let Err(e) = data {
|
||||||
|
warn!("couldn't deserialize message: {:?}", e);
|
||||||
|
if let Some(replyto) = msg.reply {
|
||||||
|
bad_request(&nats, replyto).await;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data = data.unwrap();
|
||||||
|
match data {
|
||||||
|
LDBNatsMessage::Command(cmd) => match cmd {
|
||||||
|
LypheDBCommand::SetKeys(kvlist, propstrat) => {
|
||||||
|
for (key, value) in kvlist.kvs {
|
||||||
|
if let Err(e) =
|
||||||
|
dbimpl::set_key(config.clone(), &key, &value, &propstrat).await
|
||||||
|
{
|
||||||
|
warn!("couldn't set key: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let reply = rmp_serde::to_vec(&LDBNatsMessage::Success).unwrap();
|
||||||
|
if let Err(e) = nats.publish(msg.reply.unwrap(), reply.into()).await {
|
||||||
|
warn!("couldn't send reply: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LypheDBCommand::GetKeys(klist) => {
|
||||||
|
let mut reply = Vec::new();
|
||||||
|
for key in klist.keys {
|
||||||
|
if let Ok(Some(value)) = dbimpl::get_key(config.clone(), &key).await {
|
||||||
|
reply.push((key, value));
|
||||||
|
} else {
|
||||||
|
warn!("couldn't get key: {:?}", key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let reply =
|
||||||
|
rmp_serde::to_vec(&LDBNatsMessage::Entries(KVList { kvs: reply }))
|
||||||
|
.unwrap();
|
||||||
|
if let Err(e) = nats.publish(msg.reply.unwrap(), reply.into()).await {
|
||||||
|
warn!("couldn't send reply: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LypheDBCommand::CountKeys(keydir) => {
|
||||||
|
let keys = dbimpl::get_keys_under_keydir_disk(&config, &keydir.key)
|
||||||
|
.await
|
||||||
|
.expect("couldn't get keys under keydir");
|
||||||
|
let mut count = 0;
|
||||||
|
for key in keys {
|
||||||
|
let value = dbimpl::get_key(config.clone(), &key)
|
||||||
|
.await
|
||||||
|
.expect("couldn't get value of key");
|
||||||
|
if value.is_some() {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let reply = rmp_serde::to_vec(&LDBNatsMessage::Count(count)).unwrap();
|
||||||
|
if let Err(e) = nats.publish(msg.reply.unwrap(), reply.into()).await {
|
||||||
|
warn!("couldn't send reply: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LypheDBCommand::GetKeyDirectory(keydir) => {
|
||||||
|
let mut reply = Vec::new();
|
||||||
|
let keys = dbimpl::get_keys_under_keydir_disk(&config, &keydir.key)
|
||||||
|
.await
|
||||||
|
.expect("couldn't get keys under keydir");
|
||||||
|
for key in keys {
|
||||||
|
let value = dbimpl::get_key(config.clone(), &key)
|
||||||
|
.await
|
||||||
|
.expect("couldn't get value of key");
|
||||||
|
if let Some(value) = value {
|
||||||
|
reply.push((key, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let reply =
|
||||||
|
rmp_serde::to_vec(&LDBNatsMessage::Entries(KVList { kvs: reply }))
|
||||||
|
.unwrap();
|
||||||
|
if let Err(e) = nats.publish(msg.reply.unwrap(), reply.into()).await {
|
||||||
|
warn!("couldn't send reply: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LypheDBCommand::DeleteKeys(klist) => {
|
||||||
|
for key in klist.keys {
|
||||||
|
if let Err(e) = dbimpl::delete_key(config.clone(), &key).await {
|
||||||
|
warn!("couldn't delete key: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let reply = rmp_serde::to_vec(&LDBNatsMessage::Success).unwrap();
|
||||||
|
if let Err(e) = nats.publish(msg.reply.unwrap(), reply.into()).await {
|
||||||
|
warn!("couldn't send reply: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
warn!("bad request, not command");
|
||||||
|
if let Some(replyto) = msg.reply {
|
||||||
|
bad_request(&nats, replyto).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("lyphedb shutting down");
|
||||||
|
}
|
27
nginx/default.conf
Normal file
27
nginx/default.conf
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
server {
|
||||||
|
listen 81;
|
||||||
|
server_name 0.0.0.0;
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
root /data/auth-frontend;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://auth-frontend:5843;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name 0.0.0.0;
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
root /data/frontend;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://frontend:5842;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ license-file = "LICENSE"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
asklyphe-common = { path = "../asklyphe-common" }
|
asklyphe-common = { path = "../asklyphe-common", features = ["foundationdb"] }
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
rmp-serde = "1.1.2"
|
rmp-serde = "1.1.2"
|
||||||
|
|
25
shell.nix
Executable file
25
shell.nix
Executable file
|
@ -0,0 +1,25 @@
|
||||||
|
{ pkgs ? import <nixpkgs> {}, lib ? pkgs.lib }:
|
||||||
|
pkgs.mkShellNoCC {
|
||||||
|
packages = with pkgs; [ rustup nats-server caddy postgresql clang pkg-config tmux /*you'll *need* tmux*/ ];
|
||||||
|
buildInputs = with pkgs; [ openssl clang foundationdb ];
|
||||||
|
LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.libclang ];
|
||||||
|
shellHook = ''
|
||||||
|
rustup install stable
|
||||||
|
rustup default stable
|
||||||
|
export RUST_LOG=debug
|
||||||
|
export NATS_URL="127.0.0.1:4222"
|
||||||
|
export NATS_CERT=$(cat cert.pem)
|
||||||
|
export NATS_KEY=$(cat key.pem)
|
||||||
|
export ASKLYPHE_URL="http://127.0.0.1:8002"
|
||||||
|
export AUTH_URL="http://127.0.0.1:8001"
|
||||||
|
export DB_URL="postgres://127.0.0.1:5432/user"
|
||||||
|
export SMTP_DISABLE=1
|
||||||
|
export SMTP_USERNAME=""
|
||||||
|
export SMTP_PASSWORD=""
|
||||||
|
export SMTP_URL=""
|
||||||
|
export POSTGRESQL_PASSWORD="user"
|
||||||
|
# lmao
|
||||||
|
echo WARNING: RUSTFLAGS="-A dead_code -A unused"
|
||||||
|
export RUSTFLAGS="-A dead_code -A unused"
|
||||||
|
'';
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ use astro_float::{BigFloat, RoundingMode};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
pub const PRECISION: usize = 1024;
|
pub const PRECISION: usize = 2048;
|
||||||
|
|
||||||
// length unit -> value in meters
|
// length unit -> value in meters
|
||||||
pub static LENGTH_STORE: Lazy<BTreeMap<LengthUnit, BigFloat>> = Lazy::new(|| {
|
pub static LENGTH_STORE: Lazy<BTreeMap<LengthUnit, BigFloat>> = Lazy::new(|| {
|
||||||
|
|
|
@ -15,7 +15,7 @@ use crate::length_units::{LengthUnit, LENGTH_NAMES, LENGTH_STORE, PRECISION};
|
||||||
use crate::unit_defs::{ConvertTo, MetricPrefix};
|
use crate::unit_defs::{ConvertTo, MetricPrefix};
|
||||||
use astro_float::{BigFloat, Consts, Radix, RoundingMode};
|
use astro_float::{BigFloat, Consts, Radix, RoundingMode};
|
||||||
|
|
||||||
pub const MAX_PRECISION: usize = 1024;
|
pub const MAX_PRECISION: usize = 2048;
|
||||||
|
|
||||||
pub mod length_units;
|
pub mod length_units;
|
||||||
pub mod unit_defs;
|
pub mod unit_defs;
|
||||||
|
|
1
vorebot/.gitignore
vendored
Normal file
1
vorebot/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
28
vorebot/Cargo.toml
Normal file
28
vorebot/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
[package]
|
||||||
|
name = "vorebot"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
asklyphe-common = { path = "../asklyphe-common" }
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
rand = "0.8.5"
|
||||||
|
rmp-serde = "1.1.2"
|
||||||
|
base64 = "0.21.7"
|
||||||
|
image = "0.24.8"
|
||||||
|
isahc = "1.7.2"
|
||||||
|
ulid = "1.0.0"
|
||||||
|
async-nats = "0.38.0"
|
||||||
|
futures = "0.3.28"
|
||||||
|
chrono = "0.4.26"
|
||||||
|
once_cell = "1.18.0"
|
||||||
|
env_logger = "0.10.0"
|
||||||
|
log = "0.4.19"
|
||||||
|
mutex-timeouts = { version = "0.3.0", features = ["tokio"] }
|
||||||
|
prometheus_exporter = "0.8.5"
|
||||||
|
thirtyfour = "0.35.0"
|
||||||
|
stopwords = "0.1.1"
|
||||||
|
texting_robots = "0.2.2"
|
400
vorebot/src/main.rs
Normal file
400
vorebot/src/main.rs
Normal file
|
@ -0,0 +1,400 @@
|
||||||
|
mod webparse;
|
||||||
|
|
||||||
|
use asklyphe_common::nats::vorebot::{
|
||||||
|
VOREBOT_NEWHOSTNAME_SERVICE, VOREBOT_SERVICE, VOREBOT_SUGGESTED_SERVICE,
|
||||||
|
};
|
||||||
|
use async_nats::jetstream;
|
||||||
|
use async_nats::jetstream::consumer::PullConsumer;
|
||||||
|
use async_nats::jetstream::stream::RetentionPolicy;
|
||||||
|
use chrono::TimeZone;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use log::{debug, error, info, warn};
|
||||||
|
use mutex_timeouts::tokio::MutexWithTimeoutAuto as Mutex;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use prometheus_exporter::prometheus::core::{AtomicF64, GenericGauge};
|
||||||
|
use prometheus_exporter::prometheus::{register_counter, register_gauge, Counter};
|
||||||
|
use std::cmp::max;
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::hash::{DefaultHasher, Hasher};
|
||||||
|
use std::io::Read;
|
||||||
|
use std::iter::Iterator;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::string::ToString;
|
||||||
|
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
use async_nats::jetstream::kv;
|
||||||
|
use stopwords::{Language, Spark, Stopwords};
|
||||||
|
use thirtyfour::{CapabilitiesHelper, DesiredCapabilities, Proxy, WebDriver};
|
||||||
|
use thirtyfour::common::capabilities::firefox::FirefoxPreferences;
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use asklyphe_common::ldb::DBConn;
|
||||||
|
use asklyphe_common::nats::vorebot::CrawlRequest;
|
||||||
|
use crate::webparse::web_parse;
|
||||||
|
|
||||||
|
pub static NATS_URL: Lazy<String> =
|
||||||
|
Lazy::new(|| std::env::var("NATS_URL").expect("NO NATS URL DEFINED"));
|
||||||
|
pub static NATS_PASSWORD: Lazy<Option<String>> = Lazy::new(|| {
|
||||||
|
std::env::var("NATS_PASSWORD").ok()
|
||||||
|
});
|
||||||
|
pub static NATS_CERT: Lazy<String> = Lazy::new(|| std::env::var("NATS_CERT").expect("NO NATS_CERT DEFINED"));
|
||||||
|
pub static NATS_KEY: Lazy<String> = Lazy::new(|| std::env::var("NATS_KEY").expect("NO NATS_KEY DEFINED"));
|
||||||
|
pub static BROWSER_THREADS: Lazy<Vec<String>> =
|
||||||
|
Lazy::new(|| std::env::var("BROWSER_THREADS").expect("PLEASE LIST BROWSER_THREADS").split(',').map(|v| v.to_string()).collect());
|
||||||
|
pub static BROWSER_PROXY: Lazy<Option<String>> = Lazy::new(|| {
|
||||||
|
std::env::var("BROWSER_PROXY").ok()
|
||||||
|
});
|
||||||
|
pub static DB_NAME: Lazy<String> =
|
||||||
|
Lazy::new(|| std::env::var("DB_NAME").expect("PLEASE ADD DB_NAME"));
|
||||||
|
|
||||||
|
// in minutes
|
||||||
|
const DEFAULT_CRAWLER_TIMEOUT: u64 = 5;
|
||||||
|
pub static CRAWLER_TIMEOUT: Lazy<u64> =
|
||||||
|
Lazy::new(|| std::env::var("CRAWLER_TIMEOUT").map(|v| v.parse::<u64>().ok()).ok().flatten().unwrap_or(DEFAULT_CRAWLER_TIMEOUT));
|
||||||
|
|
||||||
|
pub static DOCUMENTS_CRAWLED: AtomicU64 = AtomicU64::new(0);
|
||||||
|
pub static LAST_MESSAGE: AtomicI64 = AtomicI64::new(0);
|
||||||
|
|
||||||
|
pub static LAST_TASK_COMPLETE: Lazy<Vec<Arc<AtomicI64>>> = Lazy::new(|| {
|
||||||
|
let max_threads: usize = BROWSER_THREADS.len();
|
||||||
|
let mut vals = vec![];
|
||||||
|
for i in 0..max_threads {
|
||||||
|
// let db = Database::default().expect("couldn't connect to foundation db!");
|
||||||
|
// DBS.lock().await.push(Arc::new(db));
|
||||||
|
vals.push(Arc::new(AtomicI64::new(chrono::Utc::now().timestamp())));
|
||||||
|
}
|
||||||
|
vals
|
||||||
|
});
|
||||||
|
|
||||||
|
pub static USER_AGENT: Lazy<String> = Lazy::new(|| {
|
||||||
|
format!(
|
||||||
|
"Vorebot/{} (compatible; Googlebot/2.1; +https://voremicrocomputers.com/crawler.html)",
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
mutex_timeouts::tokio::GLOBAL_TOKIO_TIMEOUT.store(72, Ordering::Relaxed);
|
||||||
|
|
||||||
|
env_logger::init();
|
||||||
|
info!("began at {}", chrono::Utc::now().to_string());
|
||||||
|
|
||||||
|
let mut nats = async_nats::ConnectOptions::new();
|
||||||
|
|
||||||
|
if let Some(password) = NATS_PASSWORD.as_ref() {
|
||||||
|
nats = nats.user_and_password("vorebot".to_string(), password.to_string());
|
||||||
|
} else {
|
||||||
|
nats = nats.add_client_certificate(NATS_CERT.as_str().into(), NATS_KEY.as_str().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let nats = nats.connect(NATS_URL.as_str())
|
||||||
|
.await;
|
||||||
|
if let Err(e) = nats {
|
||||||
|
error!("FATAL ERROR, COULDN'T CONNECT TO NATS: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let nats = nats.unwrap();
|
||||||
|
let dbconn = DBConn::new(nats.clone(), DB_NAME.to_string());
|
||||||
|
let nats = jetstream::new(nats);
|
||||||
|
|
||||||
|
// fixme: remove this once we have proper site suggestion interface
|
||||||
|
let args = std::env::args().collect::<Vec<_>>();
|
||||||
|
if let Some(suggestion_site) = args.get(1) {
|
||||||
|
let damping = args.get(2).map(|v| v.parse::<f64>().expect("BAD FP")).unwrap_or(0.45);
|
||||||
|
warn!("suggesting {} with damping {}", suggestion_site, damping);
|
||||||
|
|
||||||
|
let ack = nats.publish(VOREBOT_SUGGESTED_SERVICE.to_string(), rmp_serde::to_vec(&CrawlRequest {
|
||||||
|
url: suggestion_site.to_string(),
|
||||||
|
damping,
|
||||||
|
}).unwrap().into()).await.unwrap();
|
||||||
|
|
||||||
|
ack.await.expect("FATAL ERROR");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tasks: Vec<(JoinHandle<()>, String, Arc<AtomicU64>)> = vec![];
|
||||||
|
let mut available_browsers: Vec<String> = BROWSER_THREADS.clone();
|
||||||
|
|
||||||
|
{
|
||||||
|
loop {
|
||||||
|
while tasks.len() < BROWSER_THREADS.len() {
|
||||||
|
let nats = nats.clone();
|
||||||
|
let browser = available_browsers.pop().expect("NO BROWSERS LEFT, THIS IS A FATAL BUG!");
|
||||||
|
let db = dbconn.clone();
|
||||||
|
let b = browser.clone();
|
||||||
|
let last_parse = Arc::new(AtomicU64::new(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()));
|
||||||
|
let lp = last_parse.clone();
|
||||||
|
tasks.push((tokio::spawn(async move {
|
||||||
|
let browser = b;
|
||||||
|
info!("using {}", browser);
|
||||||
|
info!("crawler spawned");
|
||||||
|
|
||||||
|
/* normal priority */
|
||||||
|
let consumer: PullConsumer = nats.get_or_create_stream(jetstream::stream::Config {
|
||||||
|
name: VOREBOT_SERVICE.to_string(),
|
||||||
|
subjects: vec![VOREBOT_SERVICE.to_string()],
|
||||||
|
retention: RetentionPolicy::WorkQueue,
|
||||||
|
..Default::default()
|
||||||
|
}).await
|
||||||
|
.expect("FATAL! FAILED TO SUBSCRIBE TO NATS!")
|
||||||
|
.get_or_create_consumer("parser", jetstream::consumer::pull::Config {
|
||||||
|
durable_name: Some("parser".to_string()),
|
||||||
|
filter_subject: VOREBOT_SERVICE.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}).await.expect("FATAL! FAILED TO SUBSCRIBE TO NATS!");
|
||||||
|
let mut messages = consumer.messages().await.expect("FATAL! FAILED TO SUBSCRIBE TO NATS!");
|
||||||
|
|
||||||
|
/* higher priority (new hostnames) */
|
||||||
|
let higher_consumer: PullConsumer = nats.get_or_create_stream(jetstream::stream::Config {
|
||||||
|
name: VOREBOT_NEWHOSTNAME_SERVICE.to_string(),
|
||||||
|
subjects: vec![VOREBOT_NEWHOSTNAME_SERVICE.to_string()],
|
||||||
|
retention: RetentionPolicy::WorkQueue,
|
||||||
|
..Default::default()
|
||||||
|
}).await
|
||||||
|
.expect("FATAL! FAILED TO SUBSCRIBE TO NATS!")
|
||||||
|
.get_or_create_consumer("highparser", jetstream::consumer::pull::Config {
|
||||||
|
durable_name: Some("highparser".to_string()),
|
||||||
|
filter_subject: VOREBOT_NEWHOSTNAME_SERVICE.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}).await.expect("FATAL! FAILED TO SUBSCRIBE TO NATS!");
|
||||||
|
let mut high_messages = higher_consumer.messages().await.expect("FATAL! FAILED TO SUBSCRIBE TO NATS!");
|
||||||
|
|
||||||
|
/* highest priority (user-suggested) */
|
||||||
|
let highest_consumer: PullConsumer = nats.get_or_create_stream(jetstream::stream::Config {
|
||||||
|
name: VOREBOT_SUGGESTED_SERVICE.to_string(),
|
||||||
|
subjects: vec![VOREBOT_SUGGESTED_SERVICE.to_string()],
|
||||||
|
retention: RetentionPolicy::WorkQueue,
|
||||||
|
..Default::default()
|
||||||
|
}).await
|
||||||
|
.expect("FATAL! FAILED TO SUBSCRIBE TO NATS!")
|
||||||
|
.get_or_create_consumer("highestparser", jetstream::consumer::pull::Config {
|
||||||
|
durable_name: Some("highestparser".to_string()),
|
||||||
|
filter_subject: VOREBOT_SUGGESTED_SERVICE.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}).await.expect("FATAL! FAILED TO SUBSCRIBE TO NATS!");
|
||||||
|
let mut highest_messages = highest_consumer.messages().await.expect("FATAL! FAILED TO SUBSCRIBE TO NATS!");
|
||||||
|
|
||||||
|
let mut prefs = FirefoxPreferences::new();
|
||||||
|
prefs.set_user_agent(USER_AGENT.to_string()).unwrap();
|
||||||
|
let mut caps = DesiredCapabilities::firefox();
|
||||||
|
caps.set_preferences(prefs).unwrap();
|
||||||
|
if let Some(proxy) = BROWSER_PROXY.as_ref() {
|
||||||
|
caps.set_proxy(Proxy::Manual {
|
||||||
|
ftp_proxy: None,
|
||||||
|
http_proxy: Some(proxy.to_string()),
|
||||||
|
ssl_proxy: Some(proxy.to_string()),
|
||||||
|
socks_proxy: None,
|
||||||
|
socks_version: None,
|
||||||
|
socks_username: None,
|
||||||
|
socks_password: None,
|
||||||
|
no_proxy: None,
|
||||||
|
}).unwrap();
|
||||||
|
}
|
||||||
|
let driver = WebDriver::new(&browser, caps).await.unwrap();
|
||||||
|
info!("crawler ready");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
lp.store(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), Ordering::Relaxed);
|
||||||
|
tokio::select! {
|
||||||
|
Some(highest) = StreamExt::next(&mut highest_messages) => {
|
||||||
|
if let Err(e) = highest {
|
||||||
|
warn!("error when recv js message! {e}");
|
||||||
|
} else {
|
||||||
|
let message = highest.unwrap();
|
||||||
|
if let Err(e) = message.ack().await {
|
||||||
|
warn!("failed acking message {e}");
|
||||||
|
}
|
||||||
|
let req = rmp_serde::from_slice::<CrawlRequest>(message.payload.as_ref());
|
||||||
|
if let Err(e) = req {
|
||||||
|
error!("BAD NATS REQUEST: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let req = req.unwrap();
|
||||||
|
info!("RECV USER SUGGESTION!");
|
||||||
|
let now = chrono::Utc::now().timestamp();
|
||||||
|
LAST_MESSAGE.store(now, Ordering::Relaxed);
|
||||||
|
let nats = nats.clone();
|
||||||
|
|
||||||
|
let mut bad = false;
|
||||||
|
|
||||||
|
driver.in_new_tab(|| async {
|
||||||
|
if web_parse(nats.clone(), db.clone(), &driver, &req.url, req.damping).await.is_err() {
|
||||||
|
warn!("temporary failure detected in parsing, requeuing");
|
||||||
|
nats.publish(VOREBOT_SERVICE.to_string(), rmp_serde::to_vec(&req).unwrap().into()).await.expect("FAILED TO REQUEUE");
|
||||||
|
bad = true;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}).await.unwrap();
|
||||||
|
|
||||||
|
if bad {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if DOCUMENTS_CRAWLED.load(Ordering::Relaxed) % 100 == 0 {
|
||||||
|
DOCUMENTS_CRAWLED.fetch_add(1, Ordering::Relaxed);
|
||||||
|
info!("crawled {} pages!", DOCUMENTS_CRAWLED.load(Ordering::Relaxed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(high) = StreamExt::next(&mut high_messages) => {
|
||||||
|
if let Err(e) = high {
|
||||||
|
warn!("error when recv js message! {e}");
|
||||||
|
} else {
|
||||||
|
let message = high.unwrap();
|
||||||
|
if let Err(e) = message.ack().await {
|
||||||
|
warn!("failed acking message {e}");
|
||||||
|
}
|
||||||
|
let req = rmp_serde::from_slice::<CrawlRequest>(message.payload.as_ref());
|
||||||
|
if let Err(e) = req {
|
||||||
|
error!("BAD NATS REQUEST: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let req = req.unwrap();
|
||||||
|
info!("RECV HIGH PRIORITY!");
|
||||||
|
let now = chrono::Utc::now().timestamp();
|
||||||
|
LAST_MESSAGE.store(now, Ordering::Relaxed);
|
||||||
|
let nats = nats.clone();
|
||||||
|
let mut bad = false;
|
||||||
|
|
||||||
|
driver.in_new_tab(|| async {
|
||||||
|
if web_parse(nats.clone(), db.clone(), &driver, &req.url, req.damping).await.is_err() {
|
||||||
|
warn!("temporary failure detected in parsing, requeuing");
|
||||||
|
nats.publish(VOREBOT_SERVICE.to_string(), rmp_serde::to_vec(&req).unwrap().into()).await.expect("FAILED TO REQUEUE");
|
||||||
|
bad = true;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}).await.unwrap();
|
||||||
|
|
||||||
|
if bad {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if DOCUMENTS_CRAWLED.load(Ordering::Relaxed) % 100 == 0 {
|
||||||
|
DOCUMENTS_CRAWLED.fetch_add(1, Ordering::Relaxed);
|
||||||
|
info!("crawled {} pages!", DOCUMENTS_CRAWLED.load(Ordering::Relaxed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(normal) = StreamExt::next(&mut messages) => {
|
||||||
|
if let Err(e) = normal {
|
||||||
|
warn!("error when recv js message! {e}");
|
||||||
|
} else {
|
||||||
|
let message = normal.unwrap();
|
||||||
|
if let Err(e) = message.ack().await {
|
||||||
|
warn!("failed acking message {e}");
|
||||||
|
}
|
||||||
|
let req = rmp_serde::from_slice::<CrawlRequest>(message.payload.as_ref());
|
||||||
|
if let Err(e) = req {
|
||||||
|
error!("BAD NATS REQUEST: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let req = req.unwrap();
|
||||||
|
let now = chrono::Utc::now().timestamp();
|
||||||
|
LAST_MESSAGE.store(now, Ordering::Relaxed);
|
||||||
|
let nats = nats.clone();
|
||||||
|
|
||||||
|
let mut hash = DefaultHasher::new();
|
||||||
|
hash.write(req.url.as_bytes());
|
||||||
|
let hash = hash.finish();
|
||||||
|
|
||||||
|
let dehomo_bucket = nats.get_key_value("dehomo").await;
|
||||||
|
let dehomo_bucket = if dehomo_bucket.is_err() {
|
||||||
|
let dehomo_bucket = nats.create_key_value(kv::Config {
|
||||||
|
bucket: "dehomo".to_string(),
|
||||||
|
description: "prevent the same url from being scraped again too quickly".to_string(),
|
||||||
|
max_age: Duration::from_secs(60*60),
|
||||||
|
..Default::default()
|
||||||
|
}).await;
|
||||||
|
if let Err(e) = dehomo_bucket {
|
||||||
|
panic!("FAILED TO CREATE DEHOMO BUCKET!!! {e}");
|
||||||
|
} else {
|
||||||
|
dehomo_bucket.unwrap()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dehomo_bucket.unwrap()
|
||||||
|
};
|
||||||
|
if dehomo_bucket.get(hash.to_string()).await.ok().flatten().map(|v| *v.first().unwrap_or(&0) == 1).unwrap_or(false) {
|
||||||
|
info!("too soon to scrape {}", req.url);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bad = false;
|
||||||
|
|
||||||
|
driver.in_new_tab(|| async {
|
||||||
|
if web_parse(nats.clone(), db.clone(), &driver, &req.url, req.damping).await.is_err() {
|
||||||
|
warn!("temporary failure detected in parsing, requeuing");
|
||||||
|
nats.publish(VOREBOT_SERVICE.to_string(), rmp_serde::to_vec(&req).unwrap().into()).await.expect("FAILED TO REQUEUE");
|
||||||
|
bad = true;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}).await.unwrap();
|
||||||
|
|
||||||
|
if bad {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
dehomo_bucket.put(hash.to_string(), vec![1u8].into()).await.expect("failed to store dehomo");
|
||||||
|
|
||||||
|
if DOCUMENTS_CRAWLED.load(Ordering::Relaxed) % 100 == 0 {
|
||||||
|
DOCUMENTS_CRAWLED.fetch_add(1, Ordering::Relaxed);
|
||||||
|
info!("crawled {} pages!", DOCUMENTS_CRAWLED.load(Ordering::Relaxed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
browser, last_parse.clone()
|
||||||
|
));
|
||||||
|
warn!("spawning new injest thread");
|
||||||
|
}
|
||||||
|
let mut tasks_to_remove = vec![];
|
||||||
|
for task in tasks.iter() {
|
||||||
|
if task.0.is_finished() {
|
||||||
|
tasks_to_remove.push(task.1.clone());
|
||||||
|
available_browsers.push(task.1.clone());
|
||||||
|
}
|
||||||
|
let last_parse = task.2.load(Ordering::Relaxed);
|
||||||
|
if SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() > (*CRAWLER_TIMEOUT * 60) + last_parse {
|
||||||
|
// task has been taking too long
|
||||||
|
warn!("task taking too long! aborting!");
|
||||||
|
task.0.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tasks.retain(|v| !tasks_to_remove.contains(&v.1));
|
||||||
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#[tokio::main]
|
||||||
|
//async fn main() {
|
||||||
|
// mutex_timeouts::tokio::GLOBAL_TOKIO_TIMEOUT.store(72, Ordering::Relaxed);
|
||||||
|
//
|
||||||
|
// env_logger::init();
|
||||||
|
// info!("began at {}", chrono::Utc::now().to_string());
|
||||||
|
//
|
||||||
|
// let nats = async_nats::connect(NATS_URL.as_str()).await;
|
||||||
|
// if let Err(e) = nats {
|
||||||
|
// error!("FATAL ERROR, COULDN'T CONNECT TO NATS: {}", e);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// let nats = nats.unwrap();
|
||||||
|
//
|
||||||
|
// let dbconn = DBConn::new(nats.clone(), "lyphedb-test");
|
||||||
|
//
|
||||||
|
// let nats = jetstream::new(nats);
|
||||||
|
//
|
||||||
|
// let mut prefs = FirefoxPreferences::new();
|
||||||
|
// prefs.set_user_agent(USER_AGENT.to_string()).unwrap();
|
||||||
|
// let mut caps = DesiredCapabilities::firefox();
|
||||||
|
// caps.set_preferences(prefs).unwrap();
|
||||||
|
// let driver = WebDriver::new(&BROWSER_THREADS[0], caps).await.unwrap();
|
||||||
|
//
|
||||||
|
// driver.in_new_tab(|| async {
|
||||||
|
// web_parse(nats.clone(), dbconn.clone(), &driver, "https://asklyphe.com/", 0.85).await.expect("Failed to run web parse");
|
||||||
|
//
|
||||||
|
// Ok(())
|
||||||
|
// }).await.unwrap();
|
||||||
|
//}
|
492
vorebot/src/webparse/mod.rs
Normal file
492
vorebot/src/webparse/mod.rs
Normal file
|
@ -0,0 +1,492 @@
|
||||||
|
use crate::USER_AGENT;
|
||||||
|
use asklyphe_common::ldb::{linkrelstore, linkstore, metastore, sitestore, titlestore, wordstore, DBConn};
|
||||||
|
use async_nats::jetstream;
|
||||||
|
use async_nats::jetstream::kv;
|
||||||
|
use futures::AsyncReadExt;
|
||||||
|
use image::EncodableLayout;
|
||||||
|
use isahc::config::RedirectPolicy;
|
||||||
|
use isahc::prelude::Configurable;
|
||||||
|
use isahc::HttpClient;
|
||||||
|
use log::{debug, error, warn};
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::hash::{DefaultHasher, Hasher};
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::{mpsc, Arc};
|
||||||
|
use std::time::Duration;
|
||||||
|
use stopwords::{Language, Spark, Stopwords};
|
||||||
|
use texting_robots::{get_robots_url, Robot};
|
||||||
|
use thirtyfour::{By, WebDriver};
|
||||||
|
use asklyphe_common::nats::vorebot::CrawlRequest;
|
||||||
|
use asklyphe_common::nats::vorebot::VOREBOT_SERVICE;
|
||||||
|
|
||||||
|
pub fn allowed_to_crawl(robotstxt: &[u8], url: &str) -> Result<bool, ()> {
|
||||||
|
let robot1 = Robot::new("Vorebot", robotstxt);
|
||||||
|
if let Err(e) = robot1 {
|
||||||
|
warn!(
|
||||||
|
"potentially malformed robots.txt ({}), not crawling {}",
|
||||||
|
e, url
|
||||||
|
);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let robot1 = robot1.unwrap();
|
||||||
|
Ok(robot1.allowed(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns Err if we cannot access a page, but the error associated with it seems temporary (i.e. it's worth trying again later)
|
||||||
|
// otherwise, returns Ok
|
||||||
|
pub async fn web_parse(
|
||||||
|
nats: jetstream::Context,
|
||||||
|
db: DBConn,
|
||||||
|
driver: &WebDriver,
|
||||||
|
url: &str,
|
||||||
|
damping: f64,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
|
||||||
|
driver.delete_all_cookies().await.map_err(|_| ())?;
|
||||||
|
let robots_bucket = nats.get_key_value("robots").await;
|
||||||
|
let robots_bucket = if robots_bucket.is_err() {
|
||||||
|
let robots_bucket = nats
|
||||||
|
.create_key_value(kv::Config {
|
||||||
|
bucket: "robots".to_string(),
|
||||||
|
description: "storage of robots.txt data for given hosts".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
if let Err(e) = robots_bucket {
|
||||||
|
error!("could not create robots.txt bucket: {}", e);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(robots_bucket.unwrap())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
robots_bucket.ok()
|
||||||
|
};
|
||||||
|
let hosts_bucket = nats.get_key_value("hosts").await;
|
||||||
|
let hosts_bucket = if hosts_bucket.is_err() {
|
||||||
|
let hosts_bucket = nats
|
||||||
|
.create_key_value(kv::Config {
|
||||||
|
bucket: "hosts".to_string(),
|
||||||
|
description: "prevent the same host from being scraped too quickly".to_string(),
|
||||||
|
max_age: Duration::from_secs(60 * 10),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
if let Err(e) = hosts_bucket {
|
||||||
|
error!("could not create hosts bucket: {}", e);
|
||||||
|
return Err(());
|
||||||
|
} else {
|
||||||
|
hosts_bucket.unwrap()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hosts_bucket.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let robots_url = get_robots_url(url);
|
||||||
|
if robots_url.is_err() {
|
||||||
|
error!("could not get a robots.txt url from {}, not crawling", url);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let robots_url = robots_url.unwrap();
|
||||||
|
let mut hash = DefaultHasher::new();
|
||||||
|
hash.write(robots_url.as_bytes());
|
||||||
|
let hash = hash.finish();
|
||||||
|
|
||||||
|
if let Ok(Some(host)) = hosts_bucket.get(hash.to_string()).await {
|
||||||
|
let count = *host.first().unwrap_or(&0);
|
||||||
|
if count > 10 {
|
||||||
|
warn!("scraping {} too quickly, avoiding for one minute", robots_url);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
hosts_bucket.put(hash.to_string(), vec![count + 1].into()).await.expect("COULDN'T INSERT INTO HOSTS BUCKET!");
|
||||||
|
} else {
|
||||||
|
hosts_bucket.put(hash.to_string(), vec![1].into()).await.expect("COULDN'T INSERT INTO HOSTS BUCKET!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut skip_robots_check = false;
|
||||||
|
if let Some(robots_bucket) = &robots_bucket {
|
||||||
|
if let Ok(Some(entry)) = robots_bucket.get(hash.to_string()).await {
|
||||||
|
if let Ok(res) = allowed_to_crawl(entry.as_bytes(), url) {
|
||||||
|
if !res {
|
||||||
|
debug!("robots.txt does not allow us to crawl {}", url);
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
skip_robots_check = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !skip_robots_check {
|
||||||
|
// check manually
|
||||||
|
debug!("checking new robots.txt \"{}\"", robots_url);
|
||||||
|
let client = HttpClient::builder()
|
||||||
|
.redirect_policy(RedirectPolicy::Limit(10))
|
||||||
|
.timeout(Duration::from_secs(60))
|
||||||
|
.build();
|
||||||
|
if let Err(e) = client {
|
||||||
|
error!("could not create new robots.txt httpclient: {}", e);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let client = client.unwrap();
|
||||||
|
let request = isahc::Request::get(&robots_url)
|
||||||
|
.header("user-agent", USER_AGENT.as_str())
|
||||||
|
.body(());
|
||||||
|
if let Err(e) = request {
|
||||||
|
error!("could not create robots.txt get request: {}", e);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let request = request.unwrap();
|
||||||
|
let response = client.send_async(request).await;
|
||||||
|
if let Err(e) = response {
|
||||||
|
warn!("could not get robots.txt page: {}", e);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let mut response = response.unwrap();
|
||||||
|
if response.status() == 429 {
|
||||||
|
// too many requests
|
||||||
|
warn!("too many requests for {}", robots_url);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
if response.status().is_server_error() {
|
||||||
|
// don't crawl at the moment
|
||||||
|
debug!("not crawling {} due to server error", robots_url);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body = "".to_string();
|
||||||
|
if let Err(e) = response.body_mut().read_to_string(&mut body).await {
|
||||||
|
warn!("could not read from robots.txt response: {}", e);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(res) = allowed_to_crawl(body.as_bytes(), url) {
|
||||||
|
if let Some(robots_bucket) = &robots_bucket {
|
||||||
|
if let Err(e) = robots_bucket
|
||||||
|
.put(hash.to_string(), body.as_bytes().to_vec().into())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!("could not put robots.txt data: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !res {
|
||||||
|
debug!("robots.txt does not allow us to crawl {}", url);
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
// we're allowed to crawl!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
debug!("handling request for {}", url);
|
||||||
|
|
||||||
|
// check for bad status codes
|
||||||
|
// fixme: i hate this solution, can we get something that actually checks the browser's request?
|
||||||
|
let client = HttpClient::builder()
|
||||||
|
.redirect_policy(RedirectPolicy::Limit(10))
|
||||||
|
.timeout(Duration::from_secs(60))
|
||||||
|
.build();
|
||||||
|
if let Err(e) = client {
|
||||||
|
error!("could not create new badstatuscode httpclient: {}", e);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let client = client.unwrap();
|
||||||
|
let request = isahc::Request::get(url)
|
||||||
|
.header("user-agent", USER_AGENT.as_str())
|
||||||
|
.body(());
|
||||||
|
if let Err(e) = request {
|
||||||
|
error!("could not create badstatuscode get request: {}", e);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let request = request.unwrap();
|
||||||
|
let response = client.send_async(request).await;
|
||||||
|
if let Err(e) = response {
|
||||||
|
warn!("could not get badstatuscode page: {}", e);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let mut response = response.unwrap();
|
||||||
|
if response.status() == 429 {
|
||||||
|
// too many requests
|
||||||
|
warn!("too many requests for {}", url);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
if response.status().is_server_error() || response.status().is_client_error() {
|
||||||
|
// don't crawl at the moment
|
||||||
|
debug!("not crawling {} due to bad status code {}", url, response.status());
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// i guess we're good
|
||||||
|
driver.goto(url).await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
let html_element = driver.find(By::Tag("html")).await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
if let Some(lang) = html_element.attr("lang").await.ok().flatten() {
|
||||||
|
if !lang.starts_with("en") && !lang.starts_with("unknown") {
|
||||||
|
// i.e. non-english language
|
||||||
|
// fixme: remove this once we start expanding to non-english-speaking markets?
|
||||||
|
warn!("skipping {} due to {} language (currently prioritizing english", url, lang);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let meta_elements = driver.find_all(By::Tag("meta")).await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
let title = driver.title().await.map_err(|_| ())?;
|
||||||
|
let mut description = None;
|
||||||
|
let mut keywords = vec![];
|
||||||
|
for elem in meta_elements {
|
||||||
|
if let Ok(Some(name)) = elem.attr("name").await {
|
||||||
|
match name.as_str() {
|
||||||
|
"description" => {
|
||||||
|
if let Ok(Some(content)) = elem.attr("content").await {
|
||||||
|
description = Some(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"keywords" => {
|
||||||
|
if let Ok(Some(content)) = elem.attr("content").await {
|
||||||
|
keywords = content
|
||||||
|
.split(',')
|
||||||
|
.map(|v| v.to_lowercase())
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = driver.find(By::Tag("body")).await.map_err(|_| ())?;
|
||||||
|
let raw_page_content = body.text().await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
async fn gather_elements_with_multiplier(
|
||||||
|
driver: &WebDriver,
|
||||||
|
wordmap: &mut BTreeMap<String, f64>,
|
||||||
|
stops: &BTreeSet<&&str>,
|
||||||
|
elements: &[&str],
|
||||||
|
multiplier: f64,
|
||||||
|
) {
|
||||||
|
let mut elms = vec![];
|
||||||
|
for tag in elements {
|
||||||
|
elms.push(driver.find_all(By::Tag(*tag)).await);
|
||||||
|
}
|
||||||
|
let elms = elms.iter().flatten().flatten().collect::<Vec<_>>();
|
||||||
|
let mut sentences = vec![];
|
||||||
|
let mut sentence_set = BTreeSet::new();
|
||||||
|
|
||||||
|
debug!("processing elements...");
|
||||||
|
for node in elms {
|
||||||
|
let _ = node.scroll_into_view().await;
|
||||||
|
let boxmodel = node.rect().await;
|
||||||
|
if boxmodel.is_err() {
|
||||||
|
// not visible
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let boxmodel = boxmodel.unwrap();
|
||||||
|
let current_text = node.text().await;
|
||||||
|
if current_text.is_err() {
|
||||||
|
// no text on this node
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let current_text = current_text.unwrap().trim().to_string();
|
||||||
|
if current_text.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let sqs = (boxmodel.width * boxmodel.height).max(1.0); // no 0 divides pls (:
|
||||||
|
let ccount = current_text.chars().count() as f64;
|
||||||
|
let cssq = if ccount > 0.0 { sqs / ccount } else { 0.0 };
|
||||||
|
if sentence_set.contains(¤t_text) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sentence_set.insert(current_text.clone());
|
||||||
|
sentences.push((current_text, cssq));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (sentence, cssq) in sentences {
|
||||||
|
let mut cssq = (cssq / 500.0).powi(2) * multiplier;
|
||||||
|
for word in sentence.split_whitespace() {
|
||||||
|
let word = word
|
||||||
|
.to_lowercase()
|
||||||
|
.trim_end_matches(|v: char| v.is_ascii_punctuation())
|
||||||
|
.to_string();
|
||||||
|
if stops.contains(&word.as_str()) {
|
||||||
|
// less valuable
|
||||||
|
cssq /= 100.0;
|
||||||
|
}
|
||||||
|
if let Some(wentry) = wordmap.get_mut(&word) {
|
||||||
|
*wentry += cssq;
|
||||||
|
} else {
|
||||||
|
if word.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
wordmap.insert(word.to_string(), cssq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut wordmap: BTreeMap<String, f64> = BTreeMap::new();
|
||||||
|
let stops: BTreeSet<_> = Spark::stopwords(Language::English)
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
debug!("headers...");
|
||||||
|
gather_elements_with_multiplier(driver, &mut wordmap, &stops, &["h1","h2","h3","h4","h5","h6"], 3.0)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
debug!("paragraphs...");
|
||||||
|
gather_elements_with_multiplier(driver, &mut wordmap, &stops, &["p","div"], 1.0).await;
|
||||||
|
|
||||||
|
let mut wordmap = wordmap.into_iter().collect::<Vec<_>>();
|
||||||
|
wordmap.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
|
||||||
|
|
||||||
|
let mut db_error_so_requeue_anyways = false;
|
||||||
|
|
||||||
|
let words = wordmap
|
||||||
|
.iter()
|
||||||
|
.map(|(word, _)| word.as_str())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
#[allow(clippy::collapsible_if)]
|
||||||
|
if !words.is_empty() {
|
||||||
|
if wordstore::add_url_to_keywords(&db, &words, url)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
warn!("couldn't add {} to keywords!", url);
|
||||||
|
db_error_so_requeue_anyways = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut metawords = keywords.iter().map(|v| v.as_str()).collect::<Vec<_>>();
|
||||||
|
let desc2 = description.clone();
|
||||||
|
let desc2 = desc2.map(|v| {
|
||||||
|
v.to_lowercase()
|
||||||
|
.split_whitespace()
|
||||||
|
.map(String::from)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
if let Some(description) = &desc2 {
|
||||||
|
for word in description {
|
||||||
|
let word = word.trim_end_matches(|v: char| v.is_ascii_punctuation());
|
||||||
|
if word.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
metawords.push(word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(clippy::collapsible_if)]
|
||||||
|
if !metawords.is_empty() {
|
||||||
|
if metastore::add_url_to_metawords(&db, &metawords, url)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
warn!("couldn't add {} to metawords!", url);
|
||||||
|
db_error_so_requeue_anyways = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut titlewords = vec![];
|
||||||
|
let title2 = title.clone();
|
||||||
|
let title2 = title2.to_lowercase();
|
||||||
|
for word in title2.split_whitespace() {
|
||||||
|
let word = word.trim_end_matches(|v: char| v.is_ascii_punctuation());
|
||||||
|
if word.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
titlewords.push(word);
|
||||||
|
}
|
||||||
|
#[allow(clippy::collapsible_if)]
|
||||||
|
if !titlewords.is_empty() {
|
||||||
|
if titlestore::add_url_to_titlewords(&db, &titlewords, url)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
warn!("couldn't add {} to titlewords!", url);
|
||||||
|
db_error_so_requeue_anyways = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sitestore::add_website(
|
||||||
|
&db,
|
||||||
|
url,
|
||||||
|
Some(title),
|
||||||
|
description,
|
||||||
|
if keywords.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(keywords)
|
||||||
|
},
|
||||||
|
&wordmap,
|
||||||
|
raw_page_content,
|
||||||
|
damping
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
warn!("couldn't add {} to sitestore!", url);
|
||||||
|
db_error_so_requeue_anyways = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("finished with main site stuff for {}", url);
|
||||||
|
|
||||||
|
let linkelms = driver.find_all(By::Tag("a")).await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
for linkelm in linkelms {
|
||||||
|
if linkelm.scroll_into_view().await.is_err() {
|
||||||
|
debug!("couldn't scroll into view!");
|
||||||
|
}
|
||||||
|
let href = linkelm.prop("href").await.map_err(|_| ())?;
|
||||||
|
if href.is_none() {
|
||||||
|
debug!("no href!");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let href = href.unwrap();
|
||||||
|
if href.contains('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let linktext = linkelm.text().await.map_err(|_| ())?.to_lowercase();
|
||||||
|
let linkimgs = linkelm.find_all(By::Tag("img")).await.map_err(|_| ())?;
|
||||||
|
let mut alts = "".to_string();
|
||||||
|
for img in linkimgs {
|
||||||
|
if let Ok(Some(alt)) = img.attr("alt").await {
|
||||||
|
alts.push_str(&alt);
|
||||||
|
alts.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let alts = alts.trim().to_lowercase();
|
||||||
|
let mut linkwords = vec![];
|
||||||
|
for word in linktext.split_whitespace() {
|
||||||
|
let word = word.trim_end_matches(|v: char| v.is_ascii_punctuation());
|
||||||
|
linkwords.push(word);
|
||||||
|
}
|
||||||
|
for word in alts.split_whitespace() {
|
||||||
|
let word = word.trim_end_matches(|v: char| v.is_ascii_punctuation());
|
||||||
|
linkwords.push(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::collapsible_if)]
|
||||||
|
if !linkwords.is_empty() {
|
||||||
|
if linkstore::add_url_to_linkwords(&db, &linkwords, &href).await.is_err() {
|
||||||
|
warn!("couldn't add {} to linkwords!", url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if linkrelstore::a_linksto_b(&db, url, &href).await.is_err() {
|
||||||
|
warn!("couldn't perform a_linksto_b (a {url} b {href})");
|
||||||
|
}
|
||||||
|
|
||||||
|
nats.publish(VOREBOT_SERVICE.to_string(), rmp_serde::to_vec(&CrawlRequest {
|
||||||
|
url: href,
|
||||||
|
damping: 0.85,
|
||||||
|
}).unwrap().into()).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let elapsed = start.elapsed().as_secs_f64();
|
||||||
|
|
||||||
|
debug!("crawled {} in {} seconds", url, elapsed);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue