Compare commits
52 Commits
Author | SHA1 | Date |
---|---|---|
projectmoon | 86df3c5d1f | |
projectmoon | 38a7e50c5c | |
projectmoon | e309fd1fc6 | |
projectmoon | 9262fe2cac | |
projectmoon | 724a781e7c | |
projectmoon | ef074beb96 | |
projectmoon | 81a69f329a | |
projectmoon | c9e7efa61d | |
projectmoon | f295f2b7b6 | |
projectmoon | 090ce9be45 | |
projectmoon | 2a6dff3e07 | |
projectmoon | 952f35d53a | |
projectmoon | 552daa4746 | |
projectmoon | c514b85510 | |
projectmoon | 6eb81f43d5 | |
projectmoon | 44b1e0f649 | |
projectmoon | a8ccdc9cce | |
projectmoon | 13ce7b3ee6 | |
projectmoon | 6f09a11586 | |
projectmoon | ee3ec18e06 | |
projectmoon | 126548d868 | |
Matthew Sparks | 7e7e9e534e | |
Matthew Sparks | 2d9853fbf0 | |
Matthew Sparks | 3d6210b32d | |
Matthew Sparks | 8b5973475f | |
Matthew Sparks | 1992ef4e08 | |
Matthew Sparks | f904e3a948 | |
Matthew Sparks | 8317f40f61 | |
Matthew Sparks | 069ee47364 | |
Matthew Sparks | dc242182f4 | |
Matthew Sparks | 15163ac11d | |
Matthew Sparks | 1860eaf378 | |
Matthew Sparks | 2654887d8c | |
projectmoon | 125f3d0cee | |
projectmoon | a4c3d34a97 | |
projectmoon | 86fbb05e54 | |
projectmoon | 661a943672 | |
projectmoon | d65715dee6 | |
projectmoon | 55a3bfb861 | |
projectmoon | 0050810182 | |
projectmoon | 3ba546d4a4 | |
projectmoon | ffded7b572 | |
projectmoon | cf93d14913 | |
projectmoon | cf6dd96b34 | |
projectmoon | c8c6f4d6f0 | |
projectmoon | 2488429edb | |
projectmoon | f68d5ffcc1 | |
projectmoon | 473e899275 | |
projectmoon | 1f03837bfe | |
projectmoon | 0059e3d133 | |
matthew | 915b82d0aa | |
projectmoon | cab856241d |
|
@ -3,7 +3,7 @@ name: build-and-test
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: test
|
- name: test
|
||||||
image: rust:1.51
|
image: rust:1.80
|
||||||
commands:
|
commands:
|
||||||
- apt-get update
|
- apt-get update
|
||||||
- apt-get install -y cmake
|
- apt-get install -y cmake
|
||||||
|
@ -14,8 +14,9 @@ steps:
|
||||||
- name: docker
|
- name: docker
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
when:
|
when:
|
||||||
branch:
|
ref:
|
||||||
- master
|
- refs/tags/v*
|
||||||
|
- refs/heads/master
|
||||||
settings:
|
settings:
|
||||||
auto_tag: true
|
auto_tag: true
|
||||||
username:
|
username:
|
||||||
|
|
|
@ -12,6 +12,3 @@ bigboy
|
||||||
.#*
|
.#*
|
||||||
*.sqlite
|
*.sqlite
|
||||||
.tmp*
|
.tmp*
|
||||||
node_modules
|
|
||||||
dist/
|
|
||||||
yarn-error.log
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,5 @@
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
"dicebot",
|
"dicebot",
|
||||||
"rpc",
|
"rpc"
|
||||||
"api",
|
|
||||||
"web-ui/crate",
|
|
||||||
]
|
]
|
||||||
|
|
14
Dockerfile
14
Dockerfile
|
@ -1,16 +1,15 @@
|
||||||
# Builder image with development dependencies.
|
# Builder image with development dependencies.
|
||||||
FROM bougyman/voidlinux:glibc as builder
|
FROM ghcr.io/void-linux/void-linux:latest-mini-x86_64 as builder
|
||||||
|
RUN xbps-install -S
|
||||||
|
RUN xbps-install -yu xbps
|
||||||
RUN xbps-install -Syu
|
RUN xbps-install -Syu
|
||||||
RUN xbps-install -Sy base-devel rustup cargo cmake wget gnupg
|
RUN xbps-install -Sy base-devel rustup cmake wget gnupg
|
||||||
RUN xbps-install -Sy openssl-devel libstdc++-devel
|
RUN xbps-install -Sy openssl-devel libstdc++-devel
|
||||||
RUN rustup-init -qy
|
RUN rustup-init -qy
|
||||||
|
|
||||||
# Install tini for signal processing and zombie killing
|
# Install tini for signal processing and zombie killing
|
||||||
ENV TINI_VERSION v0.19.0
|
ENV TINI_VERSION v0.19.0
|
||||||
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/local/bin/tini
|
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/local/bin/tini
|
||||||
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini.asc /tini.asc
|
|
||||||
RUN gpg --batch --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7 \
|
|
||||||
&& gpg --batch --verify /tini.asc /usr/local/bin/tini
|
|
||||||
RUN chmod +x /usr/local/bin/tini
|
RUN chmod +x /usr/local/bin/tini
|
||||||
|
|
||||||
# Build dicebot
|
# Build dicebot
|
||||||
|
@ -20,7 +19,10 @@ ADD . ./
|
||||||
RUN . /root/.cargo/env && cargo build --release
|
RUN . /root/.cargo/env && cargo build --release
|
||||||
|
|
||||||
# Final image
|
# Final image
|
||||||
FROM bougyman/voidlinux:tiny
|
FROM ghcr.io/void-linux/void-linux:latest-mini-x86_64
|
||||||
|
RUN xbps-install -S
|
||||||
|
RUN xbps-install -yu xbps
|
||||||
|
RUN xbps-install -Syu
|
||||||
RUN xbps-install -Sy ca-certificates libstdc++
|
RUN xbps-install -Sy ca-certificates libstdc++
|
||||||
COPY --from=builder \
|
COPY --from=builder \
|
||||||
/root/src/target/release/dicebot \
|
/root/src/target/release/dicebot \
|
||||||
|
|
48
README.md
48
README.md
|
@ -1,6 +1,7 @@
|
||||||
# Tenebrous Dicebot
|
# Tenebrous Dicebot
|
||||||
|
|
||||||
[![Build Status](https://drone.agnos.is/api/badges/projectmoon/tenebrous-dicebot/status.svg)](https://drone.agnos.is/projectmoon/tenebrous-dicebot)
|
[![Build Status](https://drone.agnos.is/api/badges/projectmoon/tenebrous-dicebot/status.svg)](https://drone.agnos.is/projectmoon/tenebrous-dicebot)
|
||||||
|
[![Matrix Chat](https://img.shields.io/matrix/tenebrous:agnos.is?label=matrix&server_fqdn=matrix.org)][matrix-room]
|
||||||
|
|
||||||
_This repository is hosted on [Agnos.is Git][main-repo] and mirrored
|
_This repository is hosted on [Agnos.is Git][main-repo] and mirrored
|
||||||
to [GitHub][github-repo]._
|
to [GitHub][github-repo]._
|
||||||
|
@ -24,6 +25,23 @@ System.
|
||||||
* Works in encrypted or unencrypted Matrix rooms.
|
* Works in encrypted or unencrypted Matrix rooms.
|
||||||
* Storing variables created by the user.
|
* Storing variables created by the user.
|
||||||
|
|
||||||
|
## Support and Community
|
||||||
|
|
||||||
|
The project has a Matrix room at [#tenebrous:agnos.is][matrix-room].
|
||||||
|
It is also possible to make a post in [GitHub
|
||||||
|
Discussions][github-discussions].
|
||||||
|
|
||||||
|
For reporting bugs, we prefer that you open an issue on
|
||||||
|
[git.agnos.is][agnosis-git-issues]. However, you may also open an
|
||||||
|
issue on [GitHub][github-issues].
|
||||||
|
|
||||||
|
### Development and Contributions
|
||||||
|
|
||||||
|
All development occurs on [git.agnos.is][main-repo]. If you wish to
|
||||||
|
contribute, please open a pull request there. In some cases, pull
|
||||||
|
requests from GitHub may be accepted. All contributions must be
|
||||||
|
licensed under [AGPL 3.0 or later][agpl] to be accepted.
|
||||||
|
|
||||||
## Building and Installation
|
## Building and Installation
|
||||||
|
|
||||||
### Docker Image
|
### Docker Image
|
||||||
|
@ -46,6 +64,17 @@ root of the repository.
|
||||||
After pulling or building the image, see [instructions on how to use
|
After pulling or building the image, see [instructions on how to use
|
||||||
the Docker image](#running-the-bot).
|
the Docker image](#running-the-bot).
|
||||||
|
|
||||||
|
### Install from crates.io
|
||||||
|
|
||||||
|
The project can be from [crates.io][crates-io]. To install it, execute
|
||||||
|
`cargo install tenebrous-dicebot`. This will make the following
|
||||||
|
executables available on your system:
|
||||||
|
|
||||||
|
* `dicebot`: Main dicebot executable.
|
||||||
|
* `dicebot-cmd`: Run dicebot commands from the command line.
|
||||||
|
* `dicebot_migrate`: Standalone database migrator (not required).
|
||||||
|
* `tonic_client`: Test client for the gRPC connection (not required).
|
||||||
|
|
||||||
### Build from Source
|
### Build from Source
|
||||||
|
|
||||||
Precompiled executables are not yet available. Clone this repository
|
Precompiled executables are not yet available. Clone this repository
|
||||||
|
@ -89,8 +118,16 @@ expressions.
|
||||||
!r 3d12 - 5d2 + 3 - 7d3 + 20d20
|
!r 3d12 - 5d2 + 3 - 7d3 + 20d20
|
||||||
```
|
```
|
||||||
|
|
||||||
This system does not yet have the capability to handle things like D&D
|
#### Keep/Drop Dice
|
||||||
5e advantage or disadvantage.
|
The bot supports either keeping the highest dice in a roll, or
|
||||||
|
dropping the highest dice in a roll. This allows the bot to handle
|
||||||
|
things like D&D 5e advantage or disadvantage.
|
||||||
|
|
||||||
|
```
|
||||||
|
!roll 2d20k1
|
||||||
|
!r 2d20dh1 + 5
|
||||||
|
!r 10d10k5 + 10d10dh5 - 2
|
||||||
|
```
|
||||||
|
|
||||||
### Storytelling System
|
### Storytelling System
|
||||||
|
|
||||||
|
@ -241,6 +278,7 @@ The most basic plans are:
|
||||||
* Perhaps some sort of character sheet integration. But for that, we
|
* Perhaps some sort of character sheet integration. But for that, we
|
||||||
would need a sheet service.
|
would need a sheet service.
|
||||||
* Use environment variables instead of config file in Docker image.
|
* Use environment variables instead of config file in Docker image.
|
||||||
|
* Per-system game rules.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
|
@ -254,3 +292,9 @@ support added for Chronicles of Darkness and Call of Cthulhu.
|
||||||
[main-repo]: https://git.agnos.is/projectmoon/tenebrous-dicebot
|
[main-repo]: https://git.agnos.is/projectmoon/tenebrous-dicebot
|
||||||
[github-repo]: https://github.com/ProjectMoon/matrix-dicebot
|
[github-repo]: https://github.com/ProjectMoon/matrix-dicebot
|
||||||
[roadmap]: https://git.agnos.is/projectmoon/tenebrous-dicebot/wiki/Roadmap
|
[roadmap]: https://git.agnos.is/projectmoon/tenebrous-dicebot/wiki/Roadmap
|
||||||
|
[crates-io]: https://crates.io/crates/tenebrous-dicebot
|
||||||
|
[matrix-room]: https://matrix.to/#/#tenebrous:agnos.is
|
||||||
|
[agnosis-git-issues]: https://git.agnos.is/projectmoon/tenebrous-dicebot/issues
|
||||||
|
[github-discussions]: https://github.com/ProjectMoon/matrix-dicebot/discussions
|
||||||
|
[github-issues]: https://github.com/ProjectMoon/matrix-dicebot/issues
|
||||||
|
[agpl]: https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "tenebrous-api"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["projectmoon <projectmoon@agnos.is>"]
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
log = "0.4"
|
|
||||||
tracing-subscriber = "0.2"
|
|
||||||
tonic = { version = "0.4" }
|
|
||||||
prost = "0.7"
|
|
||||||
thiserror = "1.0"
|
|
||||||
substring = "1.4"
|
|
||||||
jsonwebtoken = "7.2"
|
|
||||||
chrono = "0.4"
|
|
||||||
serde = {version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = {version = "1.0" }
|
|
||||||
tenebrous-rpc = { path = "../rpc" }
|
|
||||||
juniper = { git = "https://github.com/graphql-rust/juniper", branch = "master" }
|
|
||||||
juniper_rocket_async = { git = "https://github.com/graphql-rust/juniper", branch = "master" }
|
|
||||||
rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] }
|
|
||||||
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", branch = "master" }
|
|
|
@ -1,10 +0,0 @@
|
||||||
[default]
|
|
||||||
address = "0.0.0.0"
|
|
||||||
port = 10000
|
|
||||||
keep_alive = 5
|
|
||||||
read_timeout = 5
|
|
||||||
write_timeout = 5
|
|
||||||
log = "normal"
|
|
||||||
limits = { forms = 32768 }
|
|
||||||
origins = [ "http://localhost:8000" ]
|
|
||||||
jwt_secret = "abc123"
|
|
File diff suppressed because one or more lines are too long
|
@ -1,9 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Webpack App</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"></head>
|
|
||||||
<body>
|
|
||||||
<script src="app.bundle.js"></script></body>
|
|
||||||
</html>
|
|
|
@ -1,90 +0,0 @@
|
||||||
use crate::auth::User;
|
|
||||||
use crate::config::create_config;
|
|
||||||
use crate::schema::{self, Context, Schema};
|
|
||||||
use log::info;
|
|
||||||
use rocket::http::Method;
|
|
||||||
use rocket::{response::content, Rocket, State};
|
|
||||||
use rocket_cors::AllowedOrigins;
|
|
||||||
use std::env;
|
|
||||||
use tracing_subscriber::filter::EnvFilter;
|
|
||||||
|
|
||||||
#[rocket::get("/")]
|
|
||||||
fn graphiql() -> content::Html<String> {
|
|
||||||
juniper_rocket_async::graphiql_source("/graphql", None)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rocket::get("/graphql?<request>")]
|
|
||||||
async fn get_graphql_handler(
|
|
||||||
context: &State<Context>,
|
|
||||||
request: juniper_rocket_async::GraphQLRequest,
|
|
||||||
schema: &State<Schema>,
|
|
||||||
) -> juniper_rocket_async::GraphQLResponse {
|
|
||||||
request.execute(&*schema, &*context).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rocket::post("/graphql", data = "<request>")]
|
|
||||||
async fn post_graphql_handler(
|
|
||||||
context: &State<Context>,
|
|
||||||
request: juniper_rocket_async::GraphQLRequest,
|
|
||||||
schema: &State<Schema>,
|
|
||||||
user: User,
|
|
||||||
) -> juniper_rocket_async::GraphQLResponse {
|
|
||||||
println!("User is {:?}", user);
|
|
||||||
request.execute(&*schema, &*context).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let filter = if env::var("RUST_LOG").is_ok() {
|
|
||||||
EnvFilter::from_default_env()
|
|
||||||
} else {
|
|
||||||
EnvFilter::new("tenebrous_api=info,tonic=info,rocket=info,rocket_cors=info")
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing_subscriber::fmt().with_env_filter(filter).init();
|
|
||||||
|
|
||||||
log::info!("Setting up gRPC connection");
|
|
||||||
let rocket = Rocket::build();
|
|
||||||
let config = create_config(&rocket);
|
|
||||||
|
|
||||||
info!("Allowed CORS origins: {}", config.allowed_origins.join(","));
|
|
||||||
|
|
||||||
//TODO move to config
|
|
||||||
let client = tenebrous_rpc::create_client("http://localhost:9090", "abc123").await?;
|
|
||||||
|
|
||||||
let context = Context {
|
|
||||||
dicebot_client: client,
|
|
||||||
};
|
|
||||||
|
|
||||||
let schema = schema::schema();
|
|
||||||
|
|
||||||
let allowed_origins = AllowedOrigins::some_exact(&config.allowed_origins);
|
|
||||||
|
|
||||||
let cors = rocket_cors::CorsOptions {
|
|
||||||
allowed_origins,
|
|
||||||
allowed_methods: vec![Method::Get, Method::Post]
|
|
||||||
.into_iter()
|
|
||||||
.map(From::from)
|
|
||||||
.collect(),
|
|
||||||
allow_credentials: true,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.to_cors()?;
|
|
||||||
|
|
||||||
let routes: Vec<rocket::Route> = {
|
|
||||||
rocket::routes![graphiql, get_graphql_handler, post_graphql_handler]
|
|
||||||
.into_iter()
|
|
||||||
.chain(crate::auth::routes().into_iter())
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
rocket
|
|
||||||
.mount("/", routes)
|
|
||||||
.attach(cors)
|
|
||||||
.manage(context)
|
|
||||||
.manage(schema)
|
|
||||||
.manage(config)
|
|
||||||
.launch()
|
|
||||||
.await
|
|
||||||
.expect("server to launch");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
154
api/src/auth.rs
154
api/src/auth.rs
|
@ -1,154 +0,0 @@
|
||||||
use crate::config::Config;
|
|
||||||
use crate::errors::ApiError;
|
|
||||||
use chrono::{Duration, Utc};
|
|
||||||
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
|
||||||
use rocket::response::status::Custom;
|
|
||||||
use rocket::{http::SameSite, request::local_cache};
|
|
||||||
use rocket::{
|
|
||||||
http::Status,
|
|
||||||
serde::{json::Json, Deserialize, Serialize},
|
|
||||||
};
|
|
||||||
use rocket::{
|
|
||||||
http::{Cookie, CookieJar},
|
|
||||||
outcome::Outcome,
|
|
||||||
};
|
|
||||||
use rocket::{
|
|
||||||
outcome::IntoOutcome,
|
|
||||||
request::{self, FromRequest, Request},
|
|
||||||
};
|
|
||||||
use rocket::{routes, Route, State};
|
|
||||||
use substring::Substring;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct User {
|
|
||||||
username: String, //TODO more state and such here.
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode_token(token: &str, config: &Config) -> Result<Claims, ApiError> {
|
|
||||||
let token_data = decode::<Claims>(
|
|
||||||
token,
|
|
||||||
&DecodingKey::from_secret(config.jwt_secret.as_bytes()),
|
|
||||||
&Validation::default(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(token_data.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for User {
|
|
||||||
type Error = ApiError;
|
|
||||||
|
|
||||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
||||||
let config: Option<&Config> = req.rocket().state();
|
|
||||||
let auth_header = req
|
|
||||||
.headers()
|
|
||||||
.get_one("Authorization")
|
|
||||||
.map(|auth| auth.substring("Bearer ".len(), auth.len()));
|
|
||||||
|
|
||||||
let token = auth_header
|
|
||||||
.zip(config)
|
|
||||||
.map(|(encoded_token, app_cfg)| decode_token(encoded_token, app_cfg))
|
|
||||||
.unwrap_or(Err(ApiError::AuthenticationDenied("username".to_string())));
|
|
||||||
|
|
||||||
match token {
|
|
||||||
Err(e) => Outcome::Failure((Status::Forbidden, e)),
|
|
||||||
Ok(token) => Outcome::Success(User {
|
|
||||||
username: token.sub,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn routes() -> Vec<Route> {
|
|
||||||
routes![login, refresh_token]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct Claims {
|
|
||||||
exp: usize,
|
|
||||||
sub: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct LoginRequest<'a> {
|
|
||||||
username: &'a str,
|
|
||||||
password: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_token<'a>(
|
|
||||||
username: &str,
|
|
||||||
expiration: Duration,
|
|
||||||
secret: &str,
|
|
||||||
) -> Result<String, ApiError> {
|
|
||||||
let expiration = Utc::now()
|
|
||||||
.checked_add_signed(expiration)
|
|
||||||
.expect("clock went awry")
|
|
||||||
.timestamp();
|
|
||||||
|
|
||||||
let claims = Claims {
|
|
||||||
exp: expiration as usize,
|
|
||||||
sub: username.to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let token = encode(
|
|
||||||
&Header::default(),
|
|
||||||
&claims,
|
|
||||||
&EncodingKey::from_secret(secret.as_bytes()),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct LoginResponse {
|
|
||||||
jwt_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A strongly-typed representation of the refresh token, used with a
|
|
||||||
/// FromRequest trait to decode it from the cookie.
|
|
||||||
struct RefreshToken(String);
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for RefreshToken {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
||||||
let token: Option<RefreshToken> = request
|
|
||||||
.cookies()
|
|
||||||
.get_private("refresh_token")
|
|
||||||
.and_then(|cookie| cookie.value().parse::<String>().ok())
|
|
||||||
.map(|t| RefreshToken(t));
|
|
||||||
|
|
||||||
token.or_forward(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rocket::post("/login", data = "<request>")]
|
|
||||||
async fn login(
|
|
||||||
request: Json<LoginRequest<'_>>,
|
|
||||||
config: &State<Config>,
|
|
||||||
cookies: &CookieJar<'_>,
|
|
||||||
) -> Result<Json<LoginResponse>, ApiError> {
|
|
||||||
let token = create_token(request.username, Duration::minutes(1), &config.jwt_secret)?;
|
|
||||||
let refresh_token = create_token(request.username, Duration::weeks(1), &config.jwt_secret)?;
|
|
||||||
|
|
||||||
let mut cookie = Cookie::new("refresh_token", refresh_token);
|
|
||||||
cookie.set_same_site(SameSite::None);
|
|
||||||
cookies.add_private(cookie);
|
|
||||||
|
|
||||||
Ok(Json(LoginResponse { jwt_token: token }))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rocket::post("/refresh")]
|
|
||||||
async fn refresh_token(
|
|
||||||
config: &State<Config>,
|
|
||||||
refresh_token: Option<RefreshToken>,
|
|
||||||
) -> Result<Json<LoginResponse>, ApiError> {
|
|
||||||
let refresh_token = refresh_token.ok_or(ApiError::RefreshTokenMissing)?;
|
|
||||||
let refresh_token = decode_token(&refresh_token.0, config)?;
|
|
||||||
|
|
||||||
//TODO check if token is valid? maybe decode takes care of it.
|
|
||||||
let token = create_token(&refresh_token.sub, Duration::minutes(1), &config.jwt_secret)?;
|
|
||||||
|
|
||||||
Ok(Json(LoginResponse { jwt_token: token }))
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
use tenebrous_api::schema;
|
|
||||||
fn main() {
|
|
||||||
println!("{}", schema::schema().as_schema_language());
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
use rocket::{Phase, Rocket};
|
|
||||||
|
|
||||||
/// Config values for the API service. Available as a rocket request
|
|
||||||
/// guard.
|
|
||||||
pub struct Config {
|
|
||||||
/// The list of origins allowed to access the service.
|
|
||||||
pub allowed_origins: Vec<String>,
|
|
||||||
|
|
||||||
/// The secret key for signing JWTs.
|
|
||||||
pub jwt_secret: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_config<T: Phase>(rocket: &Rocket<T>) -> Config {
|
|
||||||
let figment = rocket.figment();
|
|
||||||
let allowed_origins: Vec<String> = figment.extract_inner("origins").expect("origins");
|
|
||||||
let jwt_secret: String = figment.extract_inner("jwt_secret").expect("jwt_secret");
|
|
||||||
|
|
||||||
Config {
|
|
||||||
allowed_origins,
|
|
||||||
jwt_secret,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
use rocket::http::ContentType;
|
|
||||||
use rocket::response::{self, Responder, Response};
|
|
||||||
use rocket::{http::Status, request::Request};
|
|
||||||
use std::io::Cursor;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum ApiError {
|
|
||||||
#[error("user account does not exist: {0}")]
|
|
||||||
UserDoesNotExist(String),
|
|
||||||
|
|
||||||
#[error("invalid password for user account: {0}")]
|
|
||||||
AuthenticationDenied(String),
|
|
||||||
|
|
||||||
#[error("authentication token missing from request")]
|
|
||||||
AuthenticationRequired,
|
|
||||||
|
|
||||||
#[error("refresh token missing from request")]
|
|
||||||
RefreshTokenMissing,
|
|
||||||
|
|
||||||
#[error("error decoding token: {0}")]
|
|
||||||
TokenDecodingError(#[from] jsonwebtoken::errors::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> Responder<'r, 'static> for ApiError {
|
|
||||||
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
|
|
||||||
let status = match self {
|
|
||||||
Self::UserDoesNotExist(_) => Status::Forbidden,
|
|
||||||
Self::AuthenticationRequired => Status::Forbidden,
|
|
||||||
Self::RefreshTokenMissing => Status::Forbidden,
|
|
||||||
Self::AuthenticationDenied(_) => Status::Forbidden,
|
|
||||||
Self::TokenDecodingError(_) => Status::InternalServerError,
|
|
||||||
};
|
|
||||||
|
|
||||||
//TODO certain errors might be too sensitive; need to filter them here.
|
|
||||||
let body = serde_json::json!({
|
|
||||||
"message": self.to_string()
|
|
||||||
})
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
Response::build()
|
|
||||||
.header(ContentType::JsonApi)
|
|
||||||
.status(status)
|
|
||||||
.sized_body(body.len(), Cursor::new(body))
|
|
||||||
.ok()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
// use std::net::SocketAddr;
|
|
||||||
// use tenebrous_rpc::protos::web_api::{
|
|
||||||
// web_api_server::{WebApi, WebApiServer},
|
|
||||||
// RoomsListReply, UserIdRequest,
|
|
||||||
// };
|
|
||||||
// use tokio::net::TcpListener;
|
|
||||||
// use tokio_stream::wrappers::TcpListenerStream;
|
|
||||||
// use tonic::{transport::Server, Request, Response, Status};
|
|
||||||
|
|
||||||
//grpc-web stuff
|
|
||||||
// struct WebApiService;
|
|
||||||
|
|
||||||
// #[tonic::async_trait]
|
|
||||||
// impl WebApi for WebApiService {
|
|
||||||
// async fn list_room(
|
|
||||||
// &self,
|
|
||||||
// request: Request<UserIdRequest>,
|
|
||||||
// ) -> Result<Response<RoomsListReply>, Status> {
|
|
||||||
// println!("Hello hopefully from a web browser");
|
|
||||||
// Ok(Response::new(RoomsListReply { rooms: vec![] }))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[tokio::main]
|
|
||||||
// pub async fn grpc_web() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
// let addr = SocketAddr::from(([127, 0, 0, 1], 10000));
|
|
||||||
// let listener = TcpListener::bind(addr).await.expect("listener");
|
|
||||||
// let url = format!("http://{}", listener.local_addr().unwrap());
|
|
||||||
// println!("Listening at {}", url);
|
|
||||||
|
|
||||||
// let svc = tonic_web::config()
|
|
||||||
// .allow_origins(vec!["http://localhost:8000"])
|
|
||||||
// .enable(WebApiServer::new(WebApiService));
|
|
||||||
|
|
||||||
// let fut = Server::builder()
|
|
||||||
// .accept_http1(true)
|
|
||||||
// .add_service(svc)
|
|
||||||
// .serve_with_incoming(TcpListenerStream::new(listener));
|
|
||||||
|
|
||||||
// fut.await?;
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
|
@ -1,5 +0,0 @@
|
||||||
pub mod api;
|
|
||||||
pub mod auth;
|
|
||||||
pub mod config;
|
|
||||||
pub mod errors;
|
|
||||||
pub mod schema;
|
|
|
@ -1,5 +0,0 @@
|
||||||
#[rocket::main]
|
|
||||||
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
tenebrous_api::api::run().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
use juniper::{
|
|
||||||
graphql_object, EmptyMutation, EmptySubscription, FieldResult, GraphQLObject, RootNode,
|
|
||||||
};
|
|
||||||
use tenebrous_rpc::protos::dicebot::GetVariableRequest;
|
|
||||||
use tenebrous_rpc::protos::dicebot::{dicebot_client::DicebotClient, UserIdRequest};
|
|
||||||
use tonic::{transport::Channel as TonicChannel, Request as TonicRequest};
|
|
||||||
|
|
||||||
/// Hide generic type behind alias.
|
|
||||||
pub type DicebotGrpcClient = DicebotClient<TonicChannel>;
|
|
||||||
|
|
||||||
/// Single room for a user.
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(description = "A matrix room.")]
|
|
||||||
struct Room {
|
|
||||||
room_id: String,
|
|
||||||
display_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List of rooms a user is in.
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(description = "List of rooms a user is in.")]
|
|
||||||
struct UserRoomList {
|
|
||||||
user_id: String,
|
|
||||||
rooms: Vec<Room>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A single user variable in a room.
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(description = "User variable in a room.")]
|
|
||||||
struct UserVariable {
|
|
||||||
room_id: String,
|
|
||||||
user_id: String,
|
|
||||||
variable_name: String,
|
|
||||||
value: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Context passed to every GraphQL function that holds stuff we need
|
|
||||||
/// (GRPC client).
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Context {
|
|
||||||
pub dicebot_client: DicebotGrpcClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marker trait to make the context object usable in GraphQL.
|
|
||||||
impl juniper::Context for Context {}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct Query;
|
|
||||||
|
|
||||||
#[graphql_object(
|
|
||||||
context = Context,
|
|
||||||
)]
|
|
||||||
impl Query {
|
|
||||||
fn api_version() -> &str {
|
|
||||||
"1.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn variable(
|
|
||||||
context: &Context,
|
|
||||||
room_id: String,
|
|
||||||
user_id: String,
|
|
||||||
variable: String,
|
|
||||||
) -> FieldResult<UserVariable> {
|
|
||||||
let request = TonicRequest::new(GetVariableRequest {
|
|
||||||
room_id,
|
|
||||||
user_id,
|
|
||||||
variable_name: variable,
|
|
||||||
});
|
|
||||||
|
|
||||||
let response = context
|
|
||||||
.dicebot_client
|
|
||||||
.clone()
|
|
||||||
.get_variable(request)
|
|
||||||
.await?
|
|
||||||
.into_inner();
|
|
||||||
|
|
||||||
Ok(UserVariable {
|
|
||||||
user_id: response.user_id,
|
|
||||||
room_id: response.room_id,
|
|
||||||
variable_name: response.variable_name,
|
|
||||||
value: response.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn user_rooms(context: &Context, user_id: String) -> FieldResult<UserRoomList> {
|
|
||||||
let request = TonicRequest::new(UserIdRequest { user_id });
|
|
||||||
|
|
||||||
let response = context
|
|
||||||
.dicebot_client
|
|
||||||
.clone()
|
|
||||||
.rooms_for_user(request)
|
|
||||||
.await?
|
|
||||||
.into_inner();
|
|
||||||
|
|
||||||
Ok(UserRoomList {
|
|
||||||
user_id: response.user_id,
|
|
||||||
rooms: response
|
|
||||||
.rooms
|
|
||||||
.into_iter()
|
|
||||||
.map(|grpc_room| Room {
|
|
||||||
room_id: grpc_room.room_id,
|
|
||||||
display_name: grpc_room.display_name,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Schema = RootNode<'static, Query, EmptyMutation<Context>, EmptySubscription<Context>>;
|
|
||||||
|
|
||||||
pub fn schema() -> Schema {
|
|
||||||
Schema::new(
|
|
||||||
Query,
|
|
||||||
EmptyMutation::<Context>::new(),
|
|
||||||
EmptySubscription::<Context>::new(),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,11 +1,12 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tenebrous-dicebot"
|
name = "tenebrous-dicebot"
|
||||||
version = "0.10.0"
|
version = "0.13.2"
|
||||||
authors = ["Taylor C. Richberger <taywee@gmx.com>", "projectmoon <projectmoon@agnos.is>"]
|
rust-version = "1.68"
|
||||||
|
authors = ["projectmoon <projectmoon@agnos.is>", "Taylor C. Richberger <taywee@gmx.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = 'AGPL-3.0-or-later'
|
license = 'AGPL-3.0-or-later'
|
||||||
description = 'An async Matrix dice bot for role-playing games'
|
description = 'An async Matrix dice bot for role-playing games'
|
||||||
readme = 'README.md'
|
readme = '../README.md'
|
||||||
repository = 'https://git.agnos.is/projectmoon/matrix-dicebot'
|
repository = 'https://git.agnos.is/projectmoon/matrix-dicebot'
|
||||||
keywords = ["games", "dice", "matrix", "bot"]
|
keywords = ["games", "dice", "matrix", "bot"]
|
||||||
categories = ["games"]
|
categories = ["games"]
|
||||||
|
@ -14,6 +15,8 @@ categories = ["games"]
|
||||||
tonic-build = "0.4"
|
tonic-build = "0.4"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# indexmap version locked fixes a dependency cycle.
|
||||||
|
# indexmap = "=1.6.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
tracing-subscriber = "0.2"
|
tracing-subscriber = "0.2"
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
|
@ -30,18 +33,19 @@ combine = "4.5"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
html2text = "0.2"
|
html2text = "0.2"
|
||||||
phf = { version = "0.8", features = ["macros"] }
|
phf = { version = "0.8", features = ["macros"] }
|
||||||
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "master" }
|
matrix-sdk = { version = "0.6" }
|
||||||
refinery = { version = "0.5", features = ["rusqlite"]}
|
refinery = { version = "0.8", features = ["rusqlite"]}
|
||||||
barrel = { version = "0.6", features = ["sqlite3"] }
|
barrel = { version = "0.7", features = ["sqlite3"] }
|
||||||
|
strum = { version = "0.22", features = ["derive"] }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
substring = "1.4"
|
substring = "1.4"
|
||||||
fuse-rust = "0.2"
|
fuse-rust = "0.2"
|
||||||
tonic = { version = "0.4" }
|
tonic = "0.4"
|
||||||
prost = "0.7"
|
prost = "0.7"
|
||||||
tenebrous-rpc = { path = "../rpc" }
|
tenebrous-rpc = { path = "../rpc", version = "0.1.0" }
|
||||||
|
|
||||||
[dependencies.sqlx]
|
[dependencies.sqlx]
|
||||||
version = "0.5"
|
version = "0.6"
|
||||||
features = [ "offline", "sqlite", "runtime-tokio-native-tls" ]
|
features = [ "offline", "sqlite", "runtime-tokio-native-tls" ]
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
# Builder image with development dependencies.
|
|
||||||
FROM bougyman/voidlinux:glibc as builder
|
|
||||||
RUN xbps-install -Syu
|
|
||||||
RUN xbps-install -Sy base-devel rustup cargo cmake wget gnupg
|
|
||||||
RUN xbps-install -Sy openssl-devel libstdc++-devel
|
|
||||||
RUN rustup-init -qy
|
|
||||||
RUN rustup component add rustfmt # Needed for protobuf building.
|
|
||||||
|
|
||||||
# Install tini for signal processing and zombie killing
|
|
||||||
ENV TINI_VERSION v0.19.0
|
|
||||||
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/local/bin/tini
|
|
||||||
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini.asc /tini.asc
|
|
||||||
RUN gpg --batch --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7 \
|
|
||||||
&& gpg --batch --verify /tini.asc /usr/local/bin/tini
|
|
||||||
RUN chmod +x /usr/local/bin/tini
|
|
||||||
|
|
||||||
# Build dicebot
|
|
||||||
RUN mkdir -p /root/src
|
|
||||||
WORKDIR /root/src
|
|
||||||
ADD . ./
|
|
||||||
RUN . /root/.cargo/env && cargo build --release
|
|
||||||
|
|
||||||
# Final image
|
|
||||||
FROM bougyman/voidlinux:tiny
|
|
||||||
RUN xbps-install -Sy ca-certificates libstdc++
|
|
||||||
COPY --from=builder \
|
|
||||||
/root/src/target/release/dicebot \
|
|
||||||
/usr/local/bin/
|
|
||||||
COPY --from=builder \
|
|
||||||
/usr/local/bin/tini \
|
|
||||||
/usr/local/bin/
|
|
||||||
|
|
||||||
ENV XDG_CACHE_HOME "/cache"
|
|
||||||
ENV DATABASE_PATH "/cache/bot-db"
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/tini", "-v", "--", "/usr/local/bin/dicebot", "/config/dicebot-config.toml" ]
|
|
|
@ -6,23 +6,52 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
//Old stuff, for regular dice rolling. To be moved elsewhere.
|
/// A basic dice roll, in XdY notation, like "1d4" or "3d6".
|
||||||
|
/// Optionally supports D&D advantage/disadvantge keep-or-drop
|
||||||
|
/// functionality.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub struct Dice {
|
pub struct Dice {
|
||||||
pub(crate) count: u32,
|
pub(crate) count: u32,
|
||||||
pub(crate) sides: u32,
|
pub(crate) sides: u32,
|
||||||
|
pub(crate) keep_drop: KeepOrDrop,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum indicating how to handle bonuses or penalties using extra
|
||||||
|
/// dice. If set to Keep, the roll will keep the highest X number of
|
||||||
|
/// dice in the roll, and add those together. If set to Drop, the
|
||||||
|
/// opposite is performed, and the lowest X number of dice are added
|
||||||
|
/// instead. If set to None, then all dice in the roll are added up as
|
||||||
|
/// normal.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum KeepOrDrop {
|
||||||
|
/// Keep only the X highest dice for adding up to the total.
|
||||||
|
Keep(u32),
|
||||||
|
|
||||||
|
/// Keep only the X lowest dice (i.e. drop the highest) for adding
|
||||||
|
/// up to the total.
|
||||||
|
Drop(u32),
|
||||||
|
|
||||||
|
/// Add up all dice in the roll for the total.
|
||||||
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Dice {
|
impl fmt::Display for Dice {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}d{}", self.count, self.sides)
|
match self.keep_drop {
|
||||||
|
KeepOrDrop::Keep(keep) => write!(f, "{}d{}k{}", self.count, self.sides, keep),
|
||||||
|
KeepOrDrop::Drop(drop) => write!(f, "{}d{}dh{}", self.count, self.sides, drop),
|
||||||
|
KeepOrDrop::None => write!(f, "{}d{}", self.count, self.sides),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dice {
|
impl Dice {
|
||||||
pub fn new(count: u32, sides: u32) -> Dice {
|
pub fn new(count: u32, sides: u32, keep_drop: KeepOrDrop) -> Dice {
|
||||||
Dice { count, sides }
|
Dice {
|
||||||
|
count,
|
||||||
|
sides,
|
||||||
|
keep_drop,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
* project.
|
* project.
|
||||||
*/
|
*/
|
||||||
use nom::bytes::complete::take_while;
|
use nom::bytes::complete::take_while;
|
||||||
|
use nom::error::ErrorKind as NomErrorKind;
|
||||||
|
use nom::Err as NomErr;
|
||||||
use nom::{
|
use nom::{
|
||||||
alt, bytes::complete::tag, character::complete::digit1, complete, many0, named,
|
alt, bytes::complete::tag, character::complete::digit1, complete, many0, named,
|
||||||
sequence::tuple, tag, IResult,
|
sequence::tuple, tag, IResult,
|
||||||
|
@ -31,13 +33,74 @@ enum Sign {
|
||||||
Minus,
|
Minus,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse a dice expression. Does not eat whitespace
|
/// Intermediate parsed value for a keep-drop expression to indicate
|
||||||
|
/// which one it is.
|
||||||
|
enum ParsedKeepOrDrop<'a> {
|
||||||
|
Keep(&'a str),
|
||||||
|
Drop(&'a str),
|
||||||
|
NotPresent,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! too_big {
|
||||||
|
($input: expr) => {
|
||||||
|
NomErr::Error(($input, NomErrorKind::TooLarge))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a dice expression. Does not eat whitespace
|
||||||
fn parse_dice(input: &str) -> IResult<&str, Dice> {
|
fn parse_dice(input: &str) -> IResult<&str, Dice> {
|
||||||
let (input, (count, _, sides)) = tuple((digit1, tag("d"), digit1))(input)?;
|
let (input, (count, _, sides)) = tuple((digit1, tag("d"), digit1))(input)?;
|
||||||
Ok((
|
let count: u32 = count.parse().map_err(|_| too_big!(count))?;
|
||||||
input,
|
let sides = sides.parse().map_err(|_| too_big!(sides))?;
|
||||||
Dice::new(count.parse().unwrap(), sides.parse().unwrap()),
|
let (input, keep_drop) = parse_keep_or_drop(input, count)?;
|
||||||
))
|
Ok((input, Dice::new(count, sides, keep_drop)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract keep/drop number as a string. Fails if the value is not a
|
||||||
|
/// string.
|
||||||
|
fn parse_keep_or_drop_text<'a>(
|
||||||
|
symbol: &'a str,
|
||||||
|
input: &'a str,
|
||||||
|
) -> IResult<&'a str, ParsedKeepOrDrop<'a>> {
|
||||||
|
let (parsed_kd, input) = match tuple::<&str, _, (_, _), _>((tag(symbol), digit1))(input) {
|
||||||
|
// if ok, one of the expressions is present
|
||||||
|
Ok((rest, (_, kd_expr))) => match symbol {
|
||||||
|
"k" => (ParsedKeepOrDrop::Keep(kd_expr), rest),
|
||||||
|
"dh" => (ParsedKeepOrDrop::Drop(kd_expr), rest),
|
||||||
|
_ => panic!("Unrecogized keep-drop symbol: {}", symbol),
|
||||||
|
},
|
||||||
|
// otherwise absent (attempt to keep all dice)
|
||||||
|
Err(_) => (ParsedKeepOrDrop::NotPresent, input),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((input, parsed_kd))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse keep/drop expression, which consits of "k" or "dh" following
|
||||||
|
/// a dice expression. For example, "1d4h3" or "1d4dh2".
|
||||||
|
fn parse_keep_or_drop<'a>(input: &'a str, count: u32) -> IResult<&'a str, KeepOrDrop> {
|
||||||
|
let (input, keep) = parse_keep_or_drop_text("k", input)?;
|
||||||
|
let (input, drop) = parse_keep_or_drop_text("dh", input)?;
|
||||||
|
|
||||||
|
use ParsedKeepOrDrop::*;
|
||||||
|
let keep_drop: KeepOrDrop = match (keep, drop) {
|
||||||
|
//Potential valid Keep expression.
|
||||||
|
(Keep(keep), NotPresent) => match keep.parse().map_err(|_| too_big!(input))? {
|
||||||
|
_i if _i > count || _i == 0 => Ok(KeepOrDrop::None),
|
||||||
|
i => Ok(KeepOrDrop::Keep(i)),
|
||||||
|
},
|
||||||
|
//Potential valid Drop expression.
|
||||||
|
(NotPresent, Drop(drop)) => match drop.parse().map_err(|_| too_big!(input))? {
|
||||||
|
_i if _i >= count => Ok(KeepOrDrop::None),
|
||||||
|
i => Ok(KeepOrDrop::Drop(i)),
|
||||||
|
},
|
||||||
|
//No Keep or Drop specified; regular behavior.
|
||||||
|
(NotPresent, NotPresent) => Ok(KeepOrDrop::None),
|
||||||
|
//Anything else is an error.
|
||||||
|
_ => Err(NomErr::Error((input, NomErrorKind::Many1))),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok((input, keep_drop))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse a single digit expression. Does not eat whitespace
|
// Parse a single digit expression. Does not eat whitespace
|
||||||
|
@ -108,16 +171,103 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn dice_test() {
|
fn dice_test() {
|
||||||
assert_eq!(parse_dice("2d4"), Ok(("", Dice::new(2, 4))));
|
assert_eq!(
|
||||||
assert_eq!(parse_dice("20d40"), Ok(("", Dice::new(20, 40))));
|
parse_dice("2d4"),
|
||||||
assert_eq!(parse_dice("8d7"), Ok(("", Dice::new(8, 7))));
|
Ok(("", Dice::new(2, 4, KeepOrDrop::None)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_dice("20d40"),
|
||||||
|
Ok(("", Dice::new(20, 40, KeepOrDrop::None)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_dice("8d7"),
|
||||||
|
Ok(("", Dice::new(8, 7, KeepOrDrop::None)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_dice("2d20k1"),
|
||||||
|
Ok(("", Dice::new(2, 20, KeepOrDrop::Keep(1))))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_dice("100d10k90"),
|
||||||
|
Ok(("", Dice::new(100, 10, KeepOrDrop::Keep(90))))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_dice("11d10k10"),
|
||||||
|
Ok(("", Dice::new(11, 10, KeepOrDrop::Keep(10))))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_dice("12d10k11"),
|
||||||
|
Ok(("", Dice::new(12, 10, KeepOrDrop::Keep(11))))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_dice("12d10k13"),
|
||||||
|
Ok(("", Dice::new(12, 10, KeepOrDrop::None)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_dice("12d10k0"),
|
||||||
|
Ok(("", Dice::new(12, 10, KeepOrDrop::None)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_dice("20d40dh5"),
|
||||||
|
Ok(("", Dice::new(20, 40, KeepOrDrop::Drop(5))))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_dice("8d7dh9"),
|
||||||
|
Ok(("", Dice::new(8, 7, KeepOrDrop::None)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_dice("8d7dh8"),
|
||||||
|
Ok(("", Dice::new(8, 7, KeepOrDrop::None)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cant_have_both_keep_and_drop_test() {
|
||||||
|
let res = parse_dice("1d4k3dh2");
|
||||||
|
assert!(res.is_err());
|
||||||
|
match res {
|
||||||
|
Err(NomErr::Error((_, kind))) => {
|
||||||
|
assert_eq!(kind, NomErrorKind::Many1);
|
||||||
|
}
|
||||||
|
_ => panic!("Got success, expected error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn big_number_of_dice_doesnt_crash_test() {
|
||||||
|
let res = parse_dice("64378631476346123874527551481376547657868536d4");
|
||||||
|
assert!(res.is_err());
|
||||||
|
match res {
|
||||||
|
Err(NomErr::Error((input, kind))) => {
|
||||||
|
assert_eq!(kind, NomErrorKind::TooLarge);
|
||||||
|
assert_eq!(input, "64378631476346123874527551481376547657868536");
|
||||||
|
}
|
||||||
|
_ => panic!("Got success, expected error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn big_number_of_sides_doesnt_crash_test() {
|
||||||
|
let res = parse_dice("1d423562312587425472658956278456298376234876");
|
||||||
|
assert!(res.is_err());
|
||||||
|
match res {
|
||||||
|
Err(NomErr::Error((input, kind))) => {
|
||||||
|
assert_eq!(kind, NomErrorKind::TooLarge);
|
||||||
|
assert_eq!(input, "423562312587425472658956278456298376234876");
|
||||||
|
}
|
||||||
|
_ => panic!("Got success, expected error"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn element_test() {
|
fn element_test() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_element(" \t\n\r\n 8d7 \n"),
|
parse_element(" \t\n\r\n 8d7 \n"),
|
||||||
Ok((" \n", Element::Dice(Dice::new(8, 7))))
|
Ok((" \n", Element::Dice(Dice::new(8, 7, KeepOrDrop::None))))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_element(" \t\n\r\n 3d20k2 \n"),
|
||||||
|
Ok((" \n", Element::Dice(Dice::new(3, 20, KeepOrDrop::Keep(2)))))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_element(" \t\n\r\n 8 \n"),
|
parse_element(" \t\n\r\n 8 \n"),
|
||||||
|
@ -139,14 +289,21 @@ mod tests {
|
||||||
parse_signed_element(" \t\n\r\n- 8d4 \n"),
|
parse_signed_element(" \t\n\r\n- 8d4 \n"),
|
||||||
Ok((
|
Ok((
|
||||||
" \n",
|
" \n",
|
||||||
SignedElement::Negative(Element::Dice(Dice::new(8, 4)))
|
SignedElement::Negative(Element::Dice(Dice::new(8, 4, KeepOrDrop::None)))
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_signed_element(" \t\n\r\n- 8d4k4 \n"),
|
||||||
|
Ok((
|
||||||
|
" \n",
|
||||||
|
SignedElement::Negative(Element::Dice(Dice::new(8, 4, KeepOrDrop::Keep(4))))
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_signed_element(" \t\n\r\n+ 8d4 \n"),
|
parse_signed_element(" \t\n\r\n+ 8d4 \n"),
|
||||||
Ok((
|
Ok((
|
||||||
" \n",
|
" \n",
|
||||||
SignedElement::Positive(Element::Dice(Dice::new(8, 4)))
|
SignedElement::Positive(Element::Dice(Dice::new(8, 4, KeepOrDrop::None)))
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -158,29 +315,43 @@ mod tests {
|
||||||
Ok((
|
Ok((
|
||||||
"",
|
"",
|
||||||
ElementExpression(vec![SignedElement::Positive(Element::Dice(Dice::new(
|
ElementExpression(vec![SignedElement::Positive(Element::Dice(Dice::new(
|
||||||
8, 4
|
8,
|
||||||
|
4,
|
||||||
|
KeepOrDrop::None
|
||||||
)))])
|
)))])
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_element_expression("\t2d20k1 + 5"),
|
||||||
|
Ok((
|
||||||
|
"",
|
||||||
|
ElementExpression(vec![
|
||||||
|
SignedElement::Positive(Element::Dice(Dice::new(2, 20, KeepOrDrop::Keep(1)))),
|
||||||
|
SignedElement::Positive(Element::Bonus(5)),
|
||||||
|
])
|
||||||
|
))
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_element_expression(" - 8d4 \n "),
|
parse_element_expression(" - 8d4 \n "),
|
||||||
Ok((
|
Ok((
|
||||||
" \n ",
|
" \n ",
|
||||||
ElementExpression(vec![SignedElement::Negative(Element::Dice(Dice::new(
|
ElementExpression(vec![SignedElement::Negative(Element::Dice(Dice::new(
|
||||||
8, 4
|
8,
|
||||||
|
4,
|
||||||
|
KeepOrDrop::None
|
||||||
)))])
|
)))])
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_element_expression("\t3d4 + 7 - 5 - 6d12 + 1d1 + 53 1d5 "),
|
parse_element_expression("\t3d4k2 + 7 - 5 - 6d12dh3 + 1d1 + 53 1d5 "),
|
||||||
Ok((
|
Ok((
|
||||||
" 1d5 ",
|
" 1d5 ",
|
||||||
ElementExpression(vec![
|
ElementExpression(vec![
|
||||||
SignedElement::Positive(Element::Dice(Dice::new(3, 4))),
|
SignedElement::Positive(Element::Dice(Dice::new(3, 4, KeepOrDrop::Keep(2)))),
|
||||||
SignedElement::Positive(Element::Bonus(7)),
|
SignedElement::Positive(Element::Bonus(7)),
|
||||||
SignedElement::Negative(Element::Bonus(5)),
|
SignedElement::Negative(Element::Bonus(5)),
|
||||||
SignedElement::Negative(Element::Dice(Dice::new(6, 12))),
|
SignedElement::Negative(Element::Dice(Dice::new(6, 12, KeepOrDrop::Drop(3)))),
|
||||||
SignedElement::Positive(Element::Dice(Dice::new(1, 1))),
|
SignedElement::Positive(Element::Dice(Dice::new(1, 1, KeepOrDrop::None))),
|
||||||
SignedElement::Positive(Element::Bonus(53)),
|
SignedElement::Positive(Element::Bonus(53)),
|
||||||
])
|
])
|
||||||
))
|
))
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* project.
|
* project.
|
||||||
*/
|
*/
|
||||||
use crate::basic::dice;
|
use crate::basic::dice;
|
||||||
|
use crate::basic::dice::KeepOrDrop;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
@ -19,15 +20,27 @@ pub trait Rolled {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct DiceRoll(pub Vec<u32>);
|
/// array of rolls in order, how many dice to keep, and how many to drop
|
||||||
|
/// keep indicates how many of the highest dice to keep
|
||||||
|
/// drop indicates how many of the highest dice to drop
|
||||||
|
pub struct DiceRoll (pub Vec<u32>, usize, usize);
|
||||||
|
|
||||||
impl DiceRoll {
|
impl DiceRoll {
|
||||||
pub fn rolls(&self) -> &[u32] {
|
pub fn rolls(&self) -> &[u32] {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn keep(&self) -> usize {
|
||||||
|
self.1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drop(&self) -> usize {
|
||||||
|
self.2
|
||||||
|
}
|
||||||
|
|
||||||
|
// only count kept dice in total
|
||||||
pub fn total(&self) -> u32 {
|
pub fn total(&self) -> u32 {
|
||||||
self.0.iter().sum()
|
self.0[self.2..self.1].iter().sum()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,11 +54,21 @@ impl fmt::Display for DiceRoll {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", self.rolled_value())?;
|
write!(f, "{}", self.rolled_value())?;
|
||||||
let rolls = self.rolls();
|
let rolls = self.rolls();
|
||||||
let mut iter = rolls.iter();
|
let keep = self.keep();
|
||||||
|
let drop = self.drop();
|
||||||
|
let mut iter = rolls.iter().enumerate();
|
||||||
if let Some(first) = iter.next() {
|
if let Some(first) = iter.next() {
|
||||||
write!(f, " ({}", first)?;
|
if drop != 0 {
|
||||||
|
write!(f, " ([{}]", first.1)?;
|
||||||
|
} else {
|
||||||
|
write!(f, " ({}", first.1)?;
|
||||||
|
}
|
||||||
for roll in iter {
|
for roll in iter {
|
||||||
write!(f, " + {}", roll)?;
|
if roll.0 >= keep || roll.0 < drop {
|
||||||
|
write!(f, " + [{}]", roll.1)?;
|
||||||
|
} else {
|
||||||
|
write!(f, " + {}", roll.1)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
write!(f, ")")?;
|
write!(f, ")")?;
|
||||||
}
|
}
|
||||||
|
@ -58,11 +81,17 @@ impl Roll for dice::Dice {
|
||||||
|
|
||||||
fn roll(&self) -> DiceRoll {
|
fn roll(&self) -> DiceRoll {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let rolls: Vec<_> = (0..self.count)
|
let mut rolls: Vec<_> = (0..self.count)
|
||||||
.map(|_| rng.gen_range(1..=self.sides))
|
.map(|_| rng.gen_range(1..=self.sides))
|
||||||
.collect();
|
.collect();
|
||||||
|
// sort rolls in descending order
|
||||||
|
rolls.sort_by(|a, b| b.cmp(a));
|
||||||
|
|
||||||
DiceRoll(rolls)
|
match self.keep_drop {
|
||||||
|
KeepOrDrop::Keep(k) => DiceRoll(rolls,k as usize, 0),
|
||||||
|
KeepOrDrop::Drop(dh) => DiceRoll(rolls,self.count as usize, dh as usize),
|
||||||
|
KeepOrDrop::None => DiceRoll(rolls,self.count as usize, 0),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,18 +227,26 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn dice_roll_display_test() {
|
fn dice_roll_display_test() {
|
||||||
assert_eq!(DiceRoll(vec![1, 3, 4]).to_string(), "8 (1 + 3 + 4)");
|
assert_eq!(DiceRoll(vec![1, 3, 4], 3, 0).to_string(), "8 (1 + 3 + 4)");
|
||||||
assert_eq!(DiceRoll(vec![]).to_string(), "0");
|
assert_eq!(DiceRoll(vec![], 0, 0).to_string(), "0");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DiceRoll(vec![4, 7, 2, 10]).to_string(),
|
DiceRoll(vec![4, 7, 2, 10], 4, 0).to_string(),
|
||||||
"23 (4 + 7 + 2 + 10)"
|
"23 (4 + 7 + 2 + 10)"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DiceRoll(vec![20, 13, 11, 10], 3, 0).to_string(),
|
||||||
|
"44 (20 + 13 + 11 + [10])"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DiceRoll(vec![20, 13, 11, 10], 4, 1).to_string(),
|
||||||
|
"34 ([20] + 13 + 11 + 10)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn element_roll_display_test() {
|
fn element_roll_display_test() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ElementRoll::Dice(DiceRoll(vec![1, 3, 4])).to_string(),
|
ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0)).to_string(),
|
||||||
"8 (1 + 3 + 4)"
|
"8 (1 + 3 + 4)"
|
||||||
);
|
);
|
||||||
assert_eq!(ElementRoll::Bonus(7).to_string(), "7");
|
assert_eq!(ElementRoll::Bonus(7).to_string(), "7");
|
||||||
|
@ -218,11 +255,11 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn signed_element_roll_display_test() {
|
fn signed_element_roll_display_test() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))).to_string(),
|
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))).to_string(),
|
||||||
"8 (1 + 3 + 4)"
|
"8 (1 + 3 + 4)"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))).to_string(),
|
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))).to_string(),
|
||||||
"-8 (1 + 3 + 4)"
|
"-8 (1 + 3 + 4)"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -239,14 +276,14 @@ mod tests {
|
||||||
fn element_expression_roll_display_test() {
|
fn element_expression_roll_display_test() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ElementExpressionRoll(vec![SignedElementRoll::Positive(ElementRoll::Dice(
|
ElementExpressionRoll(vec![SignedElementRoll::Positive(ElementRoll::Dice(
|
||||||
DiceRoll(vec![1, 3, 4])
|
DiceRoll(vec![1, 3, 4], 3, 0)
|
||||||
)),])
|
)),])
|
||||||
.to_string(),
|
.to_string(),
|
||||||
"8 (1 + 3 + 4)"
|
"8 (1 + 3 + 4)"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ElementExpressionRoll(vec![SignedElementRoll::Negative(ElementRoll::Dice(
|
ElementExpressionRoll(vec![SignedElementRoll::Negative(ElementRoll::Dice(
|
||||||
DiceRoll(vec![1, 3, 4])
|
DiceRoll(vec![1, 3, 4], 3, 0)
|
||||||
)),])
|
)),])
|
||||||
.to_string(),
|
.to_string(),
|
||||||
"-8 (1 + 3 + 4)"
|
"-8 (1 + 3 + 4)"
|
||||||
|
@ -263,8 +300,8 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ElementExpressionRoll(vec![
|
ElementExpressionRoll(vec![
|
||||||
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))),
|
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))),
|
||||||
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 2]))),
|
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 2], 2, 0))),
|
||||||
SignedElementRoll::Positive(ElementRoll::Bonus(4)),
|
SignedElementRoll::Positive(ElementRoll::Bonus(4)),
|
||||||
SignedElementRoll::Negative(ElementRoll::Bonus(7)),
|
SignedElementRoll::Negative(ElementRoll::Bonus(7)),
|
||||||
])
|
])
|
||||||
|
@ -273,13 +310,33 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ElementExpressionRoll(vec![
|
ElementExpressionRoll(vec![
|
||||||
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))),
|
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))),
|
||||||
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 2]))),
|
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 2], 2, 0))),
|
||||||
SignedElementRoll::Negative(ElementRoll::Bonus(4)),
|
SignedElementRoll::Negative(ElementRoll::Bonus(4)),
|
||||||
SignedElementRoll::Positive(ElementRoll::Bonus(7)),
|
SignedElementRoll::Positive(ElementRoll::Bonus(7)),
|
||||||
])
|
])
|
||||||
.to_string(),
|
.to_string(),
|
||||||
"-2 (-8 (1 + 3 + 4) + 3 (1 + 2) - 4 + 7)"
|
"-2 (-8 (1 + 3 + 4) + 3 (1 + 2) - 4 + 7)"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ElementExpressionRoll(vec![
|
||||||
|
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![4, 3, 1], 3, 0))),
|
||||||
|
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![12, 2], 1, 0))),
|
||||||
|
SignedElementRoll::Negative(ElementRoll::Bonus(4)),
|
||||||
|
SignedElementRoll::Positive(ElementRoll::Bonus(7)),
|
||||||
|
])
|
||||||
|
.to_string(),
|
||||||
|
"7 (-8 (4 + 3 + 1) + 12 (12 + [2]) - 4 + 7)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ElementExpressionRoll(vec![
|
||||||
|
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![4, 3, 1], 3, 1))),
|
||||||
|
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![12, 2], 2, 0))),
|
||||||
|
SignedElementRoll::Negative(ElementRoll::Bonus(4)),
|
||||||
|
SignedElementRoll::Positive(ElementRoll::Bonus(7)),
|
||||||
|
])
|
||||||
|
.to_string(),
|
||||||
|
"13 (-4 ([4] + 3 + 1) + 14 (12 + 2) - 4 + 7)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use matrix_sdk::identifiers::room_id;
|
use matrix_sdk::ruma::room_id;
|
||||||
use matrix_sdk::Client;
|
use matrix_sdk::Client;
|
||||||
use tenebrous_dicebot::commands;
|
use tenebrous_dicebot::commands;
|
||||||
use tenebrous_dicebot::commands::ResponseExtractor;
|
use tenebrous_dicebot::commands::ResponseExtractor;
|
||||||
|
@ -29,7 +29,7 @@ async fn main() -> Result<(), BotError> {
|
||||||
let context = Context {
|
let context = Context {
|
||||||
db,
|
db,
|
||||||
account: Account::default(),
|
account: Account::default(),
|
||||||
matrix_client: Client::new(homeserver).expect("Could not create matrix client"),
|
matrix_client: Client::new(homeserver).await.expect("Could not create matrix client"),
|
||||||
origin_room: RoomContext {
|
origin_room: RoomContext {
|
||||||
id: &room_id!("!fakeroomid:example.com"),
|
id: &room_id!("!fakeroomid:example.com"),
|
||||||
display_name: "fake room".to_owned(),
|
display_name: "fake room".to_owned(),
|
||||||
|
|
|
@ -21,7 +21,7 @@ async fn init(config_path: &str) -> Result<(Arc<Config>, Database, Client), BotE
|
||||||
let cfg = Arc::new(cfg);
|
let cfg = Arc::new(cfg);
|
||||||
let sqlite_path = format!("{}/dicebot.sqlite", cfg.database_path());
|
let sqlite_path = format!("{}/dicebot.sqlite", cfg.database_path());
|
||||||
let db = Database::new(&sqlite_path).await?;
|
let db = Database::new(&sqlite_path).await?;
|
||||||
let client = tenebrous_dicebot::matrix::create_client(&cfg)?;
|
let client = tenebrous_dicebot::matrix::create_client(&cfg).await?;
|
||||||
Ok((cfg, db, client))
|
Ok((cfg, db, client))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ async fn run() -> Result<(), BotError> {
|
||||||
|
|
||||||
match try_join!(bot, grpc) {
|
match try_join!(bot, grpc) {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => error!("Error: {}", e),
|
Err(e) => error!("Error: {:?}", e),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use tenebrous_rpc::protos::dicebot::UserIdRequest;
|
use tenebrous_rpc::protos::dicebot::UserIdRequest;
|
||||||
use tenebrous_rpc::protos::dicebot::{dicebot_client::DicebotClient, GetVariableRequest};
|
use tenebrous_rpc::protos::dicebot::{dicebot_client::DicebotClient};
|
||||||
use tonic::{metadata::MetadataValue, transport::Channel, Request};
|
use tonic::{metadata::MetadataValue, transport::Channel, Request};
|
||||||
|
|
||||||
async fn create_client(
|
async fn create_client(
|
||||||
|
@ -10,6 +10,7 @@ async fn create_client(
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let bearer = MetadataValue::from_str(&format!("Bearer {}", shared_secret))?;
|
let bearer = MetadataValue::from_str(&format!("Bearer {}", shared_secret))?;
|
||||||
|
|
||||||
let client = DicebotClient::with_interceptor(channel, move |mut req: Request<()>| {
|
let client = DicebotClient::with_interceptor(channel, move |mut req: Request<()>| {
|
||||||
req.metadata_mut().insert("authorization", bearer.clone());
|
req.metadata_mut().insert("authorization", bearer.clone());
|
||||||
Ok(req)
|
Ok(req)
|
||||||
|
@ -22,21 +23,11 @@ async fn create_client(
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut client = create_client("example-key").await?;
|
let mut client = create_client("example-key").await?;
|
||||||
|
|
||||||
// let request = tonic::Request::new(GetVariableRequest {
|
|
||||||
// user_id: "@projectmoon:agnos.is".into(),
|
|
||||||
// room_id: "!agICWvldGfuCywUVUM:agnos.is".into(),
|
|
||||||
// variable_name: "stuff".into(),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// let response = client.get_variable(request).await?.into_inner();
|
|
||||||
|
|
||||||
let request = tonic::Request::new(UserIdRequest {
|
let request = tonic::Request::new(UserIdRequest {
|
||||||
user_id: "@projectmoon:agnos.is".into(),
|
user_id: "@projectmoon:agnos.is".into(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let response = client.rooms_for_user(request).await?.into_inner();
|
let response = client.rooms_for_user(request).await?.into_inner();
|
||||||
// println!("RESPONSE={:?}", response);
|
|
||||||
// println!("User friendly response is: {:?}", response.value);
|
|
||||||
println!("Rooms: {:?}", response.rooms);
|
println!("Rooms: {:?}", response.rooms);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,8 @@ use crate::{
|
||||||
models::Account,
|
models::Account,
|
||||||
};
|
};
|
||||||
use futures::stream::{self, StreamExt};
|
use futures::stream::{self, StreamExt};
|
||||||
use matrix_sdk::{
|
use matrix_sdk::ruma::{OwnedEventId, RoomId};
|
||||||
self,
|
use matrix_sdk::{self, room::Joined, Client};
|
||||||
identifiers::{EventId, RoomId},
|
|
||||||
room::Joined,
|
|
||||||
Client,
|
|
||||||
};
|
|
||||||
use std::clone::Clone;
|
use std::clone::Clone;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
@ -24,7 +20,7 @@ pub(super) async fn handle_single_result(
|
||||||
cmd_result: &ExecutionResult,
|
cmd_result: &ExecutionResult,
|
||||||
respond_to: &str,
|
respond_to: &str,
|
||||||
room: &Joined,
|
room: &Joined,
|
||||||
event_id: EventId,
|
event_id: OwnedEventId,
|
||||||
) {
|
) {
|
||||||
let html = cmd_result.message_html(respond_to);
|
let html = cmd_result.message_html(respond_to);
|
||||||
let plain = cmd_result.message_plain(respond_to);
|
let plain = cmd_result.message_plain(respond_to);
|
||||||
|
@ -112,9 +108,9 @@ fn get_account_active_room(client: &Client, account: &Account) -> Result<Option<
|
||||||
let active_room = account
|
let active_room = account
|
||||||
.registered_user()
|
.registered_user()
|
||||||
.and_then(|u| u.active_room.as_deref())
|
.and_then(|u| u.active_room.as_deref())
|
||||||
.map(|room_id| RoomId::try_from(room_id))
|
.map(|room_id| <&RoomId>::try_from(room_id))
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.and_then(|active_room_id| client.get_joined_room(&active_room_id));
|
.and_then(|active_room_id| client.get_joined_room(active_room_id));
|
||||||
|
|
||||||
Ok(active_room)
|
Ok(active_room)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,32 +2,25 @@ use super::DiceBot;
|
||||||
use crate::db::sqlite::Database;
|
use crate::db::sqlite::Database;
|
||||||
use crate::db::Rooms;
|
use crate::db::Rooms;
|
||||||
use crate::error::BotError;
|
use crate::error::BotError;
|
||||||
use async_trait::async_trait;
|
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use matrix_sdk::{
|
use matrix_sdk::ruma::events::room::member::RoomMemberEventContent;
|
||||||
self,
|
use matrix_sdk::ruma::events::{StrippedStateEvent, SyncMessageLikeEvent};
|
||||||
events::{
|
use matrix_sdk::{self, room::Room, ruma::events::room::message::RoomMessageEventContent};
|
||||||
room::member::MemberEventContent,
|
use matrix_sdk::{Client, DisplayName};
|
||||||
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
|
|
||||||
StrippedStateEvent, SyncMessageEvent,
|
|
||||||
},
|
|
||||||
room::Room,
|
|
||||||
EventHandler,
|
|
||||||
};
|
|
||||||
use std::ops::Sub;
|
use std::ops::Sub;
|
||||||
|
use std::time::UNIX_EPOCH;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
use std::{clone::Clone, time::UNIX_EPOCH};
|
|
||||||
|
|
||||||
/// Check if a message is recent enough to actually process. If the
|
/// Check if a message is recent enough to actually process. If the
|
||||||
/// message is within "oldest_message_age" seconds, this function
|
/// message is within "oldest_message_age" seconds, this function
|
||||||
/// returns true. If it's older than that, it returns false and logs a
|
/// returns true. If it's older than that, it returns false and logs a
|
||||||
/// debug message.
|
/// debug message.
|
||||||
fn check_message_age(
|
fn check_message_age(
|
||||||
event: &SyncMessageEvent<MessageEventContent>,
|
event: &SyncMessageLikeEvent<RoomMessageEventContent>,
|
||||||
oldest_message_age: u64,
|
oldest_message_age: u64,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let sending_time = event
|
let sending_time = event
|
||||||
.origin_server_ts
|
.origin_server_ts()
|
||||||
.to_system_time()
|
.to_system_time()
|
||||||
.unwrap_or(UNIX_EPOCH);
|
.unwrap_or(UNIX_EPOCH);
|
||||||
|
|
||||||
|
@ -54,7 +47,7 @@ fn check_message_age(
|
||||||
/// the bot left and rejoined quickly.
|
/// the bot left and rejoined quickly.
|
||||||
async fn should_process_message<'a>(
|
async fn should_process_message<'a>(
|
||||||
bot: &DiceBot,
|
bot: &DiceBot,
|
||||||
event: &SyncMessageEvent<MessageEventContent>,
|
event: &SyncMessageLikeEvent<RoomMessageEventContent>,
|
||||||
) -> Result<(String, String), BotError> {
|
) -> Result<(String, String), BotError> {
|
||||||
//Ignore messages that are older than configured duration.
|
//Ignore messages that are older than configured duration.
|
||||||
if !check_message_age(event, bot.config.oldest_message_age()) {
|
if !check_message_age(event, bot.config.oldest_message_age()) {
|
||||||
|
@ -68,23 +61,29 @@ async fn should_process_message<'a>(
|
||||||
return Err(BotError::ShouldNotProcessError);
|
return Err(BotError::ShouldNotProcessError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (msg_body, sender_username) = if let SyncMessageEvent {
|
let msg_body: String = event
|
||||||
content:
|
.as_original()
|
||||||
MessageEventContent {
|
.map(|e| e.content.body())
|
||||||
msgtype: MessageType::Text(TextMessageEventContent { body, .. }),
|
.map(str::to_string)
|
||||||
..
|
.unwrap_or_else(|| String::new());
|
||||||
},
|
|
||||||
sender,
|
let sender_username: String = format!(
|
||||||
..
|
"@{}:{}",
|
||||||
} = event
|
event.sender().localpart(),
|
||||||
{
|
event.sender().server_name()
|
||||||
(
|
);
|
||||||
body.clone(),
|
|
||||||
format!("@{}:{}", sender.localpart(), sender.server_name()),
|
// Do not process messages from the bot itself. Otherwise it might
|
||||||
)
|
// try to execute its own commands.
|
||||||
} else {
|
let bot_username = bot
|
||||||
(String::new(), String::new())
|
.client
|
||||||
};
|
.user_id()
|
||||||
|
.map(|u| format!("@{}:{}", u.localpart(), u.server_name()))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if sender_username == bot_username {
|
||||||
|
return Err(BotError::ShouldNotProcessError);
|
||||||
|
}
|
||||||
|
|
||||||
Ok((msg_body, sender_username))
|
Ok((msg_body, sender_username))
|
||||||
}
|
}
|
||||||
|
@ -101,15 +100,10 @@ async fn should_process_event(db: &Database, room_id: &str, event_id: &str) -> b
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This event emitter listens for messages with dice rolling commands.
|
pub(super) async fn on_stripped_state_member(
|
||||||
/// Originally adapted from the matrix-rust-sdk examples.
|
event: StrippedStateEvent<RoomMemberEventContent>,
|
||||||
#[async_trait]
|
client: Client,
|
||||||
impl EventHandler for DiceBot {
|
|
||||||
async fn on_stripped_state_member(
|
|
||||||
&self,
|
|
||||||
room: Room,
|
room: Room,
|
||||||
event: &StrippedStateEvent<MemberEventContent>,
|
|
||||||
_: Option<MemberEventContent>,
|
|
||||||
) {
|
) {
|
||||||
let room = match room {
|
let room = match room {
|
||||||
Room::Invited(invited_room) => invited_room,
|
Room::Invited(invited_room) => invited_room,
|
||||||
|
@ -122,37 +116,48 @@ impl EventHandler for DiceBot {
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Autojoining room {}",
|
"Autojoining room {}",
|
||||||
room.display_name().await.ok().unwrap_or_default()
|
room.display_name()
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.unwrap_or_else(|| DisplayName::Named("[error]".to_string()))
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = self.client.join_room_by_id(&room.room_id()).await {
|
if let Err(e) = client.join_room_by_id(&room.room_id()).await {
|
||||||
warn!("Could not join room: {}", e.to_string())
|
warn!("Could not join room: {}", e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
|
pub(super) async fn on_room_message(
|
||||||
|
event: SyncMessageLikeEvent<RoomMessageEventContent>,
|
||||||
|
room: Room,
|
||||||
|
bot: DiceBot,
|
||||||
|
) {
|
||||||
let room = match room {
|
let room = match room {
|
||||||
Room::Joined(joined_room) => joined_room,
|
Room::Joined(joined_room) => joined_room,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let room_id = room.room_id().as_str();
|
let room_id = room.room_id().as_str();
|
||||||
if !should_process_event(&self.db, room_id, event.event_id.as_str()).await {
|
if !should_process_event(&bot.db, room_id, event.event_id().as_str()).await {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (msg_body, sender_username) =
|
let (msg_body, sender_username) =
|
||||||
if let Ok((msg_body, sender_username)) = should_process_message(self, &event).await {
|
if let Ok((msg_body, sender_username)) = should_process_message(&bot, &event).await {
|
||||||
(msg_body, sender_username)
|
(msg_body, sender_username)
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let results = self
|
let results = bot
|
||||||
.execute_commands(&room, &sender_username, &msg_body)
|
.execute_commands(&room, &sender_username, &msg_body)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
self.handle_results(&room, &sender_username, event.event_id.clone(), results)
|
bot.handle_results(
|
||||||
|
&room,
|
||||||
|
&sender_username,
|
||||||
|
event.event_id().to_owned(),
|
||||||
|
results,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -5,7 +5,12 @@ use crate::db::DbState;
|
||||||
use crate::error::BotError;
|
use crate::error::BotError;
|
||||||
use crate::state::DiceBotState;
|
use crate::state::DiceBotState;
|
||||||
use log::info;
|
use log::info;
|
||||||
use matrix_sdk::{self, identifiers::EventId, room::Joined, Client, SyncSettings};
|
use matrix_sdk::room::Room;
|
||||||
|
use matrix_sdk::ruma::events::room::message::RoomMessageEventContent;
|
||||||
|
use matrix_sdk::ruma::events::SyncMessageLikeEvent;
|
||||||
|
use matrix_sdk::ruma::OwnedEventId;
|
||||||
|
use matrix_sdk::{self, room::Joined, Client};
|
||||||
|
use matrix_sdk::config::SyncSettings;
|
||||||
use std::clone::Clone;
|
use std::clone::Clone;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
@ -18,6 +23,7 @@ const MAX_COMMANDS_PER_MESSAGE: usize = 50;
|
||||||
|
|
||||||
/// The DiceBot struct represents an active dice bot. The bot is not
|
/// The DiceBot struct represents an active dice bot. The bot is not
|
||||||
/// connected to Matrix until its run() function is called.
|
/// connected to Matrix until its run() function is called.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct DiceBot {
|
pub struct DiceBot {
|
||||||
/// A reference to the configuration read in on application start.
|
/// A reference to the configuration read in on application start.
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
|
@ -63,12 +69,14 @@ impl DiceBot {
|
||||||
let device_id: Option<String> = self.db.get_device_id().await?;
|
let device_id: Option<String> = self.db.get_device_id().await?;
|
||||||
let device_id: Option<&str> = device_id.as_deref();
|
let device_id: Option<&str> = device_id.as_deref();
|
||||||
|
|
||||||
client
|
let no_device_ld_login = || client.login_username(username, password);
|
||||||
.login(username, password, device_id, Some("matrix dice bot"))
|
let device_id_login = |id| client.login_username(username, password).device_id(id);
|
||||||
.await?;
|
let login = device_id.map_or_else(no_device_ld_login, device_id_login);
|
||||||
|
|
||||||
|
login.send().await?;
|
||||||
|
|
||||||
if device_id.is_none() {
|
if device_id.is_none() {
|
||||||
let device_id = client.device_id().await.ok_or(BotError::NoDeviceIdFound)?;
|
let device_id = client.device_id().ok_or(BotError::NoDeviceIdFound)?;
|
||||||
self.db.set_device_id(device_id.as_str()).await?;
|
self.db.set_device_id(device_id.as_str()).await?;
|
||||||
info!("Recorded new device ID: {}", device_id.as_str());
|
info!("Recorded new device ID: {}", device_id.as_str());
|
||||||
} else {
|
} else {
|
||||||
|
@ -79,19 +87,35 @@ impl DiceBot {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn bind_events(&self) {
|
||||||
|
//on room message: need closure to pass bot ref in.
|
||||||
|
self.client
|
||||||
|
.add_event_handler({
|
||||||
|
let bot: DiceBot = self.clone();
|
||||||
|
move |event: SyncMessageLikeEvent<RoomMessageEventContent>, room: Room| {
|
||||||
|
let bot = bot.clone();
|
||||||
|
async move { event_handlers::on_room_message(event, room, bot).await }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//auto-join handler
|
||||||
|
self.client
|
||||||
|
.add_event_handler(event_handlers::on_stripped_state_member);
|
||||||
|
}
|
||||||
|
|
||||||
/// Logs the bot in to Matrix and listens for events until program
|
/// Logs the bot in to Matrix and listens for events until program
|
||||||
/// terminated, or a panic occurs. Originally adapted from the
|
/// terminated, or a panic occurs. Originally adapted from the
|
||||||
/// matrix-rust-sdk command bot example.
|
/// matrix-rust-sdk command bot example.
|
||||||
pub async fn run(self) -> Result<(), BotError> {
|
pub async fn run(self) -> Result<(), BotError> {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
self.login(&client).await?;
|
self.login(&client).await?;
|
||||||
|
self.bind_events().await;
|
||||||
|
|
||||||
client.set_event_handler(Box::new(self)).await;
|
|
||||||
info!("Listening for commands");
|
info!("Listening for commands");
|
||||||
|
|
||||||
// TODO replace with sync_with_callback for cleaner shutdown
|
// TODO replace with sync_with_callback for cleaner shutdown
|
||||||
// process.
|
// process.
|
||||||
client.sync(SyncSettings::default()).await;
|
client.sync(SyncSettings::default()).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +145,7 @@ impl DiceBot {
|
||||||
&self,
|
&self,
|
||||||
room: &Joined,
|
room: &Joined,
|
||||||
sender_username: &str,
|
sender_username: &str,
|
||||||
event_id: EventId,
|
event_id: OwnedEventId,
|
||||||
results: Vec<(String, ExecutionResult)>,
|
results: Vec<(String, ExecutionResult)>,
|
||||||
) {
|
) {
|
||||||
if results.len() >= 1 {
|
if results.len() >= 1 {
|
||||||
|
|
|
@ -332,7 +332,7 @@ mod tests {
|
||||||
macro_rules! dummy_room {
|
macro_rules! dummy_room {
|
||||||
() => {
|
() => {
|
||||||
crate::context::RoomContext {
|
crate::context::RoomContext {
|
||||||
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
|
id: &matrix_sdk::ruma::room_id!("!fakeroomid:example.com"),
|
||||||
display_name: "displayname".to_owned(),
|
display_name: "displayname".to_owned(),
|
||||||
secure: false,
|
secure: false,
|
||||||
}
|
}
|
||||||
|
@ -485,7 +485,7 @@ mod tests {
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
account: crate::models::Account::default(),
|
account: crate::models::Account::default(),
|
||||||
db: db,
|
db: db,
|
||||||
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
|
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
|
||||||
origin_room: dummy_room!(),
|
origin_room: dummy_room!(),
|
||||||
active_room: dummy_room!(),
|
active_room: dummy_room!(),
|
||||||
username: "username",
|
username: "username",
|
||||||
|
@ -527,7 +527,7 @@ mod tests {
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
account: crate::models::Account::default(),
|
account: crate::models::Account::default(),
|
||||||
db: db,
|
db: db,
|
||||||
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
|
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
|
||||||
origin_room: dummy_room!(),
|
origin_room: dummy_room!(),
|
||||||
active_room: dummy_room!(),
|
active_room: dummy_room!(),
|
||||||
username: "username",
|
username: "username",
|
||||||
|
@ -566,7 +566,7 @@ mod tests {
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
account: crate::models::Account::default(),
|
account: crate::models::Account::default(),
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
|
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
|
||||||
origin_room: dummy_room!(),
|
origin_room: dummy_room!(),
|
||||||
active_room: dummy_room!(),
|
active_room: dummy_room!(),
|
||||||
username: "username",
|
username: "username",
|
||||||
|
|
|
@ -45,13 +45,13 @@ pub fn parse_modifiers(input: &str) -> Result<DicePoolModifiers, DiceParsingErro
|
||||||
let (result, rest) = parser.parse(input)?;
|
let (result, rest) = parser.parse(input)?;
|
||||||
|
|
||||||
if rest.len() == 0 {
|
if rest.len() == 0 {
|
||||||
convert_to_info(&result)
|
convert_to_modifiers(&result)
|
||||||
} else {
|
} else {
|
||||||
Err(DiceParsingError::UnconsumedInput)
|
Err(DiceParsingError::UnconsumedInput)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_to_info(parsed: &Vec<ParsedInfo>) -> Result<DicePoolModifiers, DiceParsingError> {
|
fn convert_to_modifiers(parsed: &Vec<ParsedInfo>) -> Result<DicePoolModifiers, DiceParsingError> {
|
||||||
use ParsedInfo::*;
|
use ParsedInfo::*;
|
||||||
if parsed.len() == 0 {
|
if parsed.len() == 0 {
|
||||||
Ok(DicePoolModifiers::default())
|
Ok(DicePoolModifiers::default())
|
||||||
|
@ -79,19 +79,8 @@ fn convert_to_info(parsed: &Vec<ParsedInfo>) -> Result<DicePoolModifiers, DicePa
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_dice_pool(input: &str) -> Result<DicePool, BotError> {
|
pub fn parse_dice_pool(input: &str) -> Result<DicePool, BotError> {
|
||||||
//The "modifiers:" part is optional. Assume amounts if no modifier
|
let (amounts, modifiers_str) = parse_amounts(input)?;
|
||||||
//section found.
|
|
||||||
let split = input.split(":").collect::<Vec<_>>();
|
|
||||||
let (modifiers_str, amounts_str) = (match split[..] {
|
|
||||||
[amounts] => Ok(("", amounts)),
|
|
||||||
[modifiers, amounts] => Ok((modifiers, amounts)),
|
|
||||||
_ => Err(BotError::DiceParsingError(
|
|
||||||
DiceParsingError::UnconsumedInput,
|
|
||||||
)),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let modifiers = parse_modifiers(modifiers_str)?;
|
let modifiers = parse_modifiers(modifiers_str)?;
|
||||||
let amounts = parse_amounts(&amounts_str)?;
|
|
||||||
Ok(DicePool::new(amounts, modifiers))
|
Ok(DicePool::new(amounts, modifiers))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +164,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dice_pool_number_with_quality() {
|
fn dice_pool_number_with_quality() {
|
||||||
let result = parse_dice_pool("n:8");
|
let result = parse_dice_pool("8 n");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap(),
|
result.unwrap(),
|
||||||
|
@ -186,7 +175,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn dice_pool_number_with_success_change() {
|
fn dice_pool_number_with_success_change() {
|
||||||
let modifiers = DicePoolModifiers::custom_exceptional_on(3);
|
let modifiers = DicePoolModifiers::custom_exceptional_on(3);
|
||||||
let result = parse_dice_pool("s3:8");
|
let result = parse_dice_pool("8 s3");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(result.unwrap(), DicePool::easy_with_modifiers(8, modifiers));
|
assert_eq!(result.unwrap(), DicePool::easy_with_modifiers(8, modifiers));
|
||||||
}
|
}
|
||||||
|
@ -194,7 +183,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn dice_pool_with_quality_and_success_change() {
|
fn dice_pool_with_quality_and_success_change() {
|
||||||
let modifiers = DicePoolModifiers::custom(DicePoolQuality::Rote, 3);
|
let modifiers = DicePoolModifiers::custom(DicePoolQuality::Rote, 3);
|
||||||
let result = parse_dice_pool("rs3:8");
|
let result = parse_dice_pool("8 rs3");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(result.unwrap(), DicePool::easy_with_modifiers(8, modifiers));
|
assert_eq!(result.unwrap(), DicePool::easy_with_modifiers(8, modifiers));
|
||||||
}
|
}
|
||||||
|
@ -224,20 +213,20 @@ mod tests {
|
||||||
|
|
||||||
let expected = DicePool::new(amounts, modifiers);
|
let expected = DicePool::new(amounts, modifiers);
|
||||||
|
|
||||||
let result = parse_dice_pool("rs3:8+10-2+varname");
|
let result = parse_dice_pool("8+10-2+varname rs3");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(result.unwrap(), expected);
|
assert_eq!(result.unwrap(), expected);
|
||||||
|
|
||||||
let result = parse_dice_pool("rs3:8+10- 2 + varname");
|
let result = parse_dice_pool("8+10- 2 + varname rs3");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(result.unwrap(), expected);
|
assert_eq!(result.unwrap(), expected);
|
||||||
|
|
||||||
let result = parse_dice_pool("rs3 : 8+ 10 -2 + varname");
|
let result = parse_dice_pool("8+ 10 -2 + varname rs3");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(result.unwrap(), expected);
|
assert_eq!(result.unwrap(), expected);
|
||||||
|
|
||||||
//This one has tabs in it.
|
//This one has tabs in it.
|
||||||
let result = parse_dice_pool(" r s3 : 8 + 10 -2 + varname");
|
let result = parse_dice_pool(" 8 + 10 -2 + varname r s3");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(result.unwrap(), expected);
|
assert_eq!(result.unwrap(), expected);
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,11 +162,12 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use management::RegisterCommand;
|
use management::RegisterCommand;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use matrix_sdk::ruma::room_id;
|
||||||
|
|
||||||
macro_rules! dummy_room {
|
macro_rules! dummy_room {
|
||||||
() => {
|
() => {
|
||||||
crate::context::RoomContext {
|
crate::context::RoomContext {
|
||||||
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
|
id: &room_id!("!fakeroomid:example.com"),
|
||||||
display_name: "displayname".to_owned(),
|
display_name: "displayname".to_owned(),
|
||||||
secure: false,
|
secure: false,
|
||||||
}
|
}
|
||||||
|
@ -176,7 +177,7 @@ mod tests {
|
||||||
macro_rules! secure_room {
|
macro_rules! secure_room {
|
||||||
() => {
|
() => {
|
||||||
crate::context::RoomContext {
|
crate::context::RoomContext {
|
||||||
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
|
id: &room_id!("!fakeroomid:example.com"),
|
||||||
display_name: "displayname".to_owned(),
|
display_name: "displayname".to_owned(),
|
||||||
secure: true,
|
secure: true,
|
||||||
}
|
}
|
||||||
|
@ -195,7 +196,7 @@ mod tests {
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
account: crate::models::Account::default(),
|
account: crate::models::Account::default(),
|
||||||
db: db,
|
db: db,
|
||||||
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
|
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
|
||||||
origin_room: secure_room!(),
|
origin_room: secure_room!(),
|
||||||
active_room: secure_room!(),
|
active_room: secure_room!(),
|
||||||
username: "myusername",
|
username: "myusername",
|
||||||
|
@ -218,7 +219,7 @@ mod tests {
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
account: crate::models::Account::default(),
|
account: crate::models::Account::default(),
|
||||||
db: db,
|
db: db,
|
||||||
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
|
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
|
||||||
origin_room: secure_room!(),
|
origin_room: secure_room!(),
|
||||||
active_room: secure_room!(),
|
active_room: secure_room!(),
|
||||||
username: "myusername",
|
username: "myusername",
|
||||||
|
@ -241,7 +242,7 @@ mod tests {
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
account: crate::models::Account::default(),
|
account: crate::models::Account::default(),
|
||||||
db: db,
|
db: db,
|
||||||
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
|
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
|
||||||
origin_room: dummy_room!(),
|
origin_room: dummy_room!(),
|
||||||
active_room: dummy_room!(),
|
active_room: dummy_room!(),
|
||||||
username: "myusername",
|
username: "myusername",
|
||||||
|
@ -264,7 +265,7 @@ mod tests {
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
account: crate::models::Account::default(),
|
account: crate::models::Account::default(),
|
||||||
db: db,
|
db: db,
|
||||||
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
|
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
|
||||||
origin_room: dummy_room!(),
|
origin_room: dummy_room!(),
|
||||||
active_room: dummy_room!(),
|
active_room: dummy_room!(),
|
||||||
username: "myusername",
|
username: "myusername",
|
||||||
|
@ -287,7 +288,7 @@ mod tests {
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
account: crate::models::Account::default(),
|
account: crate::models::Account::default(),
|
||||||
db: db,
|
db: db,
|
||||||
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
|
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
|
||||||
origin_room: dummy_room!(),
|
origin_room: dummy_room!(),
|
||||||
active_room: dummy_room!(),
|
active_room: dummy_room!(),
|
||||||
username: "myusername",
|
username: "myusername",
|
||||||
|
|
|
@ -221,9 +221,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pool_whitespace_test() {
|
fn pool_whitespace_test() {
|
||||||
parse_command("!pool ns3:8 ").expect("was error");
|
parse_command("!pool 8 ns3 ").expect("was error");
|
||||||
parse_command(" !pool ns3:8").expect("was error");
|
parse_command(" !pool 8 ns3").expect("was error");
|
||||||
parse_command(" !pool ns3:8 ").expect("was error");
|
parse_command(" !pool 8 ns3 ").expect("was error");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::matrix;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use fuse_rust::{Fuse, FuseProperty, Fuseable};
|
use fuse_rust::{Fuse, FuseProperty, Fuseable};
|
||||||
use futures::stream::{self, StreamExt, TryStreamExt};
|
use futures::stream::{self, StreamExt, TryStreamExt};
|
||||||
use matrix_sdk::{identifiers::UserId, Client};
|
use matrix_sdk::{ruma::OwnedUserId, Client};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
/// Holds matrix room ID and display name as strings, for use with
|
/// Holds matrix room ID and display name as strings, for use with
|
||||||
|
@ -62,13 +62,13 @@ async fn get_rooms_for_user(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
) -> Result<Vec<RoomNameAndId>, BotError> {
|
) -> Result<Vec<RoomNameAndId>, BotError> {
|
||||||
let user_id = UserId::try_from(user_id)?;
|
let user_id = OwnedUserId::try_from(user_id)?;
|
||||||
let rooms_for_user = matrix::get_rooms_for_user(client, &user_id).await?;
|
let rooms_for_user = matrix::get_rooms_for_user(client, &user_id).await?;
|
||||||
let mut rooms_for_user: Vec<RoomNameAndId> = stream::iter(rooms_for_user)
|
let mut rooms_for_user: Vec<RoomNameAndId> = stream::iter(rooms_for_user)
|
||||||
.filter_map(|room| async move {
|
.filter_map(|room| async move {
|
||||||
Some(room.display_name().await.map(|room_name| RoomNameAndId {
|
Some(room.display_name().await.map(|room_name| RoomNameAndId {
|
||||||
id: room.room_id().to_string(),
|
id: room.room_id().to_string(),
|
||||||
name: room_name,
|
name: room_name.to_string(),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect()
|
.try_collect()
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::db::sqlite::Database;
|
use crate::db::sqlite::Database;
|
||||||
use crate::error::BotError;
|
use crate::error::BotError;
|
||||||
use crate::models::Account;
|
use crate::models::Account;
|
||||||
use matrix_sdk::identifiers::{RoomId, UserId};
|
|
||||||
use matrix_sdk::room::Joined;
|
use matrix_sdk::room::Joined;
|
||||||
|
use matrix_sdk::ruma::{RoomId, UserId};
|
||||||
use matrix_sdk::Client;
|
use matrix_sdk::Client;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
@ -48,15 +48,16 @@ impl RoomContext<'_> {
|
||||||
// TODO is_direct is a hack; the bot should set eligible rooms
|
// TODO is_direct is a hack; the bot should set eligible rooms
|
||||||
// to Direct Message upon joining, if other contact has
|
// to Direct Message upon joining, if other contact has
|
||||||
// requested it. Waiting on SDK support.
|
// requested it. Waiting on SDK support.
|
||||||
let display_name = room
|
let display_name =
|
||||||
|
room
|
||||||
.display_name()
|
.display_name()
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.unwrap_or_default()
|
.map(|d| d.to_string())
|
||||||
.to_string();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let sending_user = UserId::try_from(sending_user)?;
|
let sending_user = <&UserId>::try_from(sending_user)?;
|
||||||
let user_in_room = room.get_member(&sending_user).await.ok().is_some();
|
let user_in_room = room.get_member(sending_user).await.ok().is_some();
|
||||||
let is_direct = room.active_members().await?.len() == 2;
|
let is_direct = room.active_members().await?.len() == 2;
|
||||||
|
|
||||||
Ok(RoomContext {
|
Ok(RoomContext {
|
||||||
|
|
|
@ -270,7 +270,7 @@ macro_rules! is_variable {
|
||||||
element: Element::Variable(_),
|
element: Element::Variable(_),
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,11 +427,12 @@ mod tests {
|
||||||
use crate::db::sqlite::Database;
|
use crate::db::sqlite::Database;
|
||||||
use crate::parser::dice::{Amount, Element, Operator};
|
use crate::parser::dice::{Amount, Element, Operator};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use matrix_sdk::ruma::room_id;
|
||||||
|
|
||||||
macro_rules! dummy_room {
|
macro_rules! dummy_room {
|
||||||
() => {
|
() => {
|
||||||
crate::context::RoomContext {
|
crate::context::RoomContext {
|
||||||
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
|
id: &room_id!("!fakeroomid:example.com"),
|
||||||
display_name: "displayname".to_owned(),
|
display_name: "displayname".to_owned(),
|
||||||
secure: false,
|
secure: false,
|
||||||
}
|
}
|
||||||
|
@ -511,7 +512,7 @@ mod tests {
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
account: crate::models::Account::default(),
|
account: crate::models::Account::default(),
|
||||||
db: db,
|
db: db,
|
||||||
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
|
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
|
||||||
origin_room: dummy_room!(),
|
origin_room: dummy_room!(),
|
||||||
active_room: dummy_room!(),
|
active_room: dummy_room!(),
|
||||||
username: "username",
|
username: "username",
|
||||||
|
@ -549,7 +550,7 @@ mod tests {
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
account: crate::models::Account::default(),
|
account: crate::models::Account::default(),
|
||||||
db: db,
|
db: db,
|
||||||
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
|
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
|
||||||
origin_room: dummy_room!(),
|
origin_room: dummy_room!(),
|
||||||
active_room: dummy_room!(),
|
active_room: dummy_room!(),
|
||||||
username: "username",
|
username: "username",
|
||||||
|
@ -587,7 +588,7 @@ mod tests {
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
account: crate::models::Account::default(),
|
account: crate::models::Account::default(),
|
||||||
db: db,
|
db: db,
|
||||||
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
|
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
|
||||||
origin_room: dummy_room!(),
|
origin_room: dummy_room!(),
|
||||||
active_room: dummy_room!(),
|
active_room: dummy_room!(),
|
||||||
username: "username",
|
username: "username",
|
||||||
|
|
|
@ -4,16 +4,13 @@ use crate::parser::dice::DiceParsingError;
|
||||||
//TOOD convert these to use parse_amounts from the common dice code.
|
//TOOD convert these to use parse_amounts from the common dice code.
|
||||||
|
|
||||||
fn parse_modifier(input: &str) -> Result<DiceRollModifier, DiceParsingError> {
|
fn parse_modifier(input: &str) -> Result<DiceRollModifier, DiceParsingError> {
|
||||||
if input.ends_with("bb") {
|
match input.trim() {
|
||||||
Ok(DiceRollModifier::TwoBonus)
|
"bb" => Ok(DiceRollModifier::TwoBonus),
|
||||||
} else if input.ends_with("b") {
|
"b" => Ok(DiceRollModifier::OneBonus),
|
||||||
Ok(DiceRollModifier::OneBonus)
|
"pp" => Ok(DiceRollModifier::TwoPenalty),
|
||||||
} else if input.ends_with("pp") {
|
"p" => Ok(DiceRollModifier::OnePenalty),
|
||||||
Ok(DiceRollModifier::TwoPenalty)
|
"" => Ok(DiceRollModifier::Normal),
|
||||||
} else if input.ends_with("p") {
|
_ => Err(DiceParsingError::InvalidModifiers),
|
||||||
Ok(DiceRollModifier::OnePenalty)
|
|
||||||
} else {
|
|
||||||
Ok(DiceRollModifier::Normal)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,32 +18,70 @@ fn parse_modifier(input: &str) -> Result<DiceRollModifier, DiceParsingError> {
|
||||||
//Split based on :, send first part to parse_modifier.
|
//Split based on :, send first part to parse_modifier.
|
||||||
//Send second part to parse_amounts
|
//Send second part to parse_amounts
|
||||||
pub fn parse_regular_roll(input: &str) -> Result<DiceRoll, DiceParsingError> {
|
pub fn parse_regular_roll(input: &str) -> Result<DiceRoll, DiceParsingError> {
|
||||||
let input: Vec<&str> = input.trim().split(":").collect();
|
let (amount, modifiers_str) = crate::parser::dice::parse_single_amount(input)?;
|
||||||
|
|
||||||
let (modifiers_str, amounts_str) = match input[..] {
|
|
||||||
[amounts] => Ok(("", amounts)),
|
|
||||||
[modifiers, amounts] => Ok((modifiers, amounts)),
|
|
||||||
_ => Err(DiceParsingError::UnconsumedInput),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let modifier = parse_modifier(modifiers_str)?;
|
let modifier = parse_modifier(modifiers_str)?;
|
||||||
let amount = crate::parser::dice::parse_single_amount(amounts_str)?;
|
|
||||||
Ok(DiceRoll { modifier, amount })
|
Ok(DiceRoll { modifier, amount })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_advancement_roll(input: &str) -> Result<AdvancementRoll, DiceParsingError> {
|
pub fn parse_advancement_roll(input: &str) -> Result<AdvancementRoll, DiceParsingError> {
|
||||||
let input = input.trim();
|
let input = input.trim();
|
||||||
let amounts = crate::parser::dice::parse_single_amount(input)?;
|
let (amounts, unconsumed_input) = crate::parser::dice::parse_single_amount(input)?;
|
||||||
|
|
||||||
|
if unconsumed_input.len() == 0 {
|
||||||
Ok(AdvancementRoll {
|
Ok(AdvancementRoll {
|
||||||
existing_skill: amounts,
|
existing_skill: amounts,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
Err(DiceParsingError::InvalidAmount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::parser::dice::{Amount, Element, Operator};
|
use crate::parser::dice::{Amount, DiceParsingError, Element, Operator};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_modifier_rejects_bad_value() {
|
||||||
|
let modifier = parse_modifier("qqq");
|
||||||
|
assert!(matches!(modifier, Err(DiceParsingError::InvalidModifiers)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_modifier_accepts_one_bonus() {
|
||||||
|
let modifier = parse_modifier("b");
|
||||||
|
assert!(matches!(modifier, Ok(DiceRollModifier::OneBonus)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_modifier_accepts_two_bonus() {
|
||||||
|
let modifier = parse_modifier("bb");
|
||||||
|
assert!(matches!(modifier, Ok(DiceRollModifier::TwoBonus)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_modifier_accepts_two_penalty() {
|
||||||
|
let modifier = parse_modifier("pp");
|
||||||
|
assert!(matches!(modifier, Ok(DiceRollModifier::TwoPenalty)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_modifier_accepts_one_penalty() {
|
||||||
|
let modifier = parse_modifier("p");
|
||||||
|
assert!(matches!(modifier, Ok(DiceRollModifier::OnePenalty)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_modifier_accepts_normal() {
|
||||||
|
let modifier = parse_modifier("");
|
||||||
|
assert!(matches!(modifier, Ok(DiceRollModifier::Normal)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_modifier_accepts_normal_unaffected_by_whitespace() {
|
||||||
|
let modifier = parse_modifier(" ");
|
||||||
|
assert!(matches!(modifier, Ok(DiceRollModifier::Normal)))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_accepts_single_number() {
|
fn regular_roll_accepts_single_number() {
|
||||||
|
@ -72,7 +107,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_accepts_two_bonus() {
|
fn regular_roll_accepts_two_bonus() {
|
||||||
let result = parse_regular_roll("bb:60");
|
let result = parse_regular_roll("60 bb");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DiceRoll {
|
DiceRoll {
|
||||||
|
@ -88,7 +123,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_accepts_one_bonus() {
|
fn regular_roll_accepts_one_bonus() {
|
||||||
let result = parse_regular_roll("b:60");
|
let result = parse_regular_roll("60 b");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DiceRoll {
|
DiceRoll {
|
||||||
|
@ -104,7 +139,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_accepts_two_penalty() {
|
fn regular_roll_accepts_two_penalty() {
|
||||||
let result = parse_regular_roll("pp:60");
|
let result = parse_regular_roll("60 pp");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DiceRoll {
|
DiceRoll {
|
||||||
|
@ -120,7 +155,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regular_roll_accepts_one_penalty() {
|
fn regular_roll_accepts_one_penalty() {
|
||||||
let result = parse_regular_roll("p:60");
|
let result = parse_regular_roll("60 p");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DiceRoll {
|
DiceRoll {
|
||||||
|
@ -140,21 +175,21 @@ mod tests {
|
||||||
assert!(parse_regular_roll(" 60").is_ok());
|
assert!(parse_regular_roll(" 60").is_ok());
|
||||||
assert!(parse_regular_roll(" 60 ").is_ok());
|
assert!(parse_regular_roll(" 60 ").is_ok());
|
||||||
|
|
||||||
assert!(parse_regular_roll("bb:60 ").is_ok());
|
assert!(parse_regular_roll("60bb ").is_ok());
|
||||||
assert!(parse_regular_roll(" bb:60").is_ok());
|
assert!(parse_regular_roll(" 60 bb").is_ok());
|
||||||
assert!(parse_regular_roll(" bb:60 ").is_ok());
|
assert!(parse_regular_roll(" 60 bb ").is_ok());
|
||||||
|
|
||||||
assert!(parse_regular_roll("b:60 ").is_ok());
|
assert!(parse_regular_roll("60b ").is_ok());
|
||||||
assert!(parse_regular_roll(" b:60").is_ok());
|
assert!(parse_regular_roll(" 60 b").is_ok());
|
||||||
assert!(parse_regular_roll(" b:60 ").is_ok());
|
assert!(parse_regular_roll(" 60 b ").is_ok());
|
||||||
|
|
||||||
assert!(parse_regular_roll("pp:60 ").is_ok());
|
assert!(parse_regular_roll("60pp ").is_ok());
|
||||||
assert!(parse_regular_roll(" pp:60").is_ok());
|
assert!(parse_regular_roll(" 60 pp").is_ok());
|
||||||
assert!(parse_regular_roll(" pp:60 ").is_ok());
|
assert!(parse_regular_roll(" 60 pp ").is_ok());
|
||||||
|
|
||||||
assert!(parse_regular_roll("p:60 ").is_ok());
|
assert!(parse_regular_roll("60p ").is_ok());
|
||||||
assert!(parse_regular_roll(" p:60").is_ok());
|
assert!(parse_regular_roll(" 60p ").is_ok());
|
||||||
assert!(parse_regular_roll(" p:60 ").is_ok());
|
assert!(parse_regular_roll(" 60 p ").is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
use crate::systems::GameSystem;
|
||||||
|
use barrel::backend::Sqlite;
|
||||||
|
use barrel::{types, types::Type, Migration};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
fn primary_id() -> Type {
|
||||||
|
types::text().unique(true).primary(true).nullable(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn migration() -> String {
|
||||||
|
let mut m = Migration::new();
|
||||||
|
|
||||||
|
//Normally we would add a CHECK clause here, but types::custom requires a 'static string.
|
||||||
|
//Which means we can't automagically generate one from the enum.
|
||||||
|
m.create_table("room_info", move |t| {
|
||||||
|
t.add_column("room_id", primary_id());
|
||||||
|
t.add_column("game_system", types::text().nullable(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
m.make::<Sqlite>()
|
||||||
|
}
|
|
@ -1,2 +1 @@
|
||||||
use refinery::include_migration_mods;
|
|
||||||
include_migration_mods!("src/db/sqlite/migrator/migrations");
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use sqlx::ConnectOptions;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub mod migrations;
|
//pub mod migrations;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum MigrationError {
|
pub enum MigrationError {
|
||||||
|
@ -16,6 +16,11 @@ pub enum MigrationError {
|
||||||
RefineryError(#[from] refinery::Error),
|
RefineryError(#[from] refinery::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod embedded {
|
||||||
|
use refinery::embed_migrations;
|
||||||
|
embed_migrations!("src/db/sqlite/migrator/migrations");
|
||||||
|
}
|
||||||
|
|
||||||
/// Run database migrations against the sqlite database.
|
/// Run database migrations against the sqlite database.
|
||||||
pub async fn migrate(db_path: &str) -> Result<(), MigrationError> {
|
pub async fn migrate(db_path: &str) -> Result<(), MigrationError> {
|
||||||
//Create database if missing.
|
//Create database if missing.
|
||||||
|
@ -28,6 +33,6 @@ pub async fn migrate(db_path: &str) -> Result<(), MigrationError> {
|
||||||
|
|
||||||
let mut conn = Config::new(ConfigDbType::Sqlite).set_db_path(db_path);
|
let mut conn = Config::new(ConfigDbType::Sqlite).set_db_path(db_path);
|
||||||
info!("Running migrations");
|
info!("Running migrations");
|
||||||
migrations::runner().run(&mut conn)?;
|
embedded::migrations::runner().run(&mut conn)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,22 +53,27 @@ impl Rooms for Database {
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::db::sqlite::Database;
|
use crate::db::sqlite::Database;
|
||||||
use crate::db::Rooms;
|
use crate::db::Rooms;
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
async fn create_db() -> Database {
|
async fn with_db<Fut>(f: impl FnOnce(Database) -> Fut)
|
||||||
|
where
|
||||||
|
Fut: Future<Output = ()>,
|
||||||
|
{
|
||||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||||
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
|
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Database::new(db_path.path().to_str().unwrap())
|
let db = Database::new(db_path.path().to_str().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap();
|
||||||
|
|
||||||
|
f(db).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn should_process_test() {
|
async fn should_process_test() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
let first_check = db
|
let first_check = db
|
||||||
.should_process("myroom", "myeventid")
|
.should_process("myroom", "myeventid")
|
||||||
.await
|
.await
|
||||||
|
@ -82,5 +87,7 @@ mod tests {
|
||||||
.expect("should_process failed in first insert");
|
.expect("should_process failed in first insert");
|
||||||
|
|
||||||
assert_eq!(second_check, false);
|
assert_eq!(second_check, false);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,22 +37,27 @@ impl DbState for Database {
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::db::sqlite::Database;
|
use crate::db::sqlite::Database;
|
||||||
use crate::db::DbState;
|
use crate::db::DbState;
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
async fn create_db() -> Database {
|
async fn with_db<Fut>(f: impl FnOnce(Database) -> Fut)
|
||||||
|
where
|
||||||
|
Fut: Future<Output = ()>,
|
||||||
|
{
|
||||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||||
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
|
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Database::new(db_path.path().to_str().unwrap())
|
let db = Database::new(db_path.path().to_str().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap();
|
||||||
|
|
||||||
|
f(db).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn set_and_get_device_id() {
|
async fn set_and_get_device_id() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
db.set_device_id("device_id")
|
db.set_device_id("device_id")
|
||||||
.await
|
.await
|
||||||
.expect("Could not set device ID");
|
.expect("Could not set device ID");
|
||||||
|
@ -61,19 +66,22 @@ mod tests {
|
||||||
|
|
||||||
assert!(device_id.is_some());
|
assert!(device_id.is_some());
|
||||||
assert_eq!(device_id.unwrap(), "device_id");
|
assert_eq!(device_id.unwrap(), "device_id");
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn no_device_id_set_returns_none() {
|
async fn no_device_id_set_returns_none() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
let device_id = db.get_device_id().await.expect("Could not get device ID");
|
let device_id = db.get_device_id().await.expect("Could not get device ID");
|
||||||
assert!(device_id.is_none());
|
assert!(device_id.is_none());
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn can_update_device_id() {
|
async fn can_update_device_id() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
db.set_device_id("device_id")
|
db.set_device_id("device_id")
|
||||||
.await
|
.await
|
||||||
.expect("Could not set device ID");
|
.expect("Could not set device ID");
|
||||||
|
@ -86,5 +94,7 @@ mod tests {
|
||||||
|
|
||||||
assert!(device_id.is_some());
|
assert!(device_id.is_some());
|
||||||
assert_eq!(device_id.unwrap(), "device_id2");
|
assert_eq!(device_id.unwrap(), "device_id2");
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,22 +91,27 @@ mod tests {
|
||||||
use crate::db::sqlite::Database;
|
use crate::db::sqlite::Database;
|
||||||
use crate::db::Users;
|
use crate::db::Users;
|
||||||
use crate::models::AccountStatus;
|
use crate::models::AccountStatus;
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
async fn create_db() -> Database {
|
async fn with_db<Fut>(f: impl FnOnce(Database) -> Fut)
|
||||||
|
where
|
||||||
|
Fut: Future<Output = ()>,
|
||||||
|
{
|
||||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||||
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
|
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Database::new(db_path.path().to_str().unwrap())
|
let db = Database::new(db_path.path().to_str().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap();
|
||||||
|
|
||||||
|
f(db).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn create_and_get_full_user_test() {
|
async fn create_and_get_full_user_test() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
let insert_result = db
|
let insert_result = db
|
||||||
.upsert_user(&User {
|
.upsert_user(&User {
|
||||||
username: "myuser".to_string(),
|
username: "myuser".to_string(),
|
||||||
|
@ -129,12 +134,13 @@ mod tests {
|
||||||
assert_eq!(user.password, Some("abc".to_string()));
|
assert_eq!(user.password, Some("abc".to_string()));
|
||||||
assert_eq!(user.account_status, AccountStatus::Registered);
|
assert_eq!(user.account_status, AccountStatus::Registered);
|
||||||
assert_eq!(user.active_room, Some("myroom".to_string()));
|
assert_eq!(user.active_room, Some("myroom".to_string()));
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn can_get_user_with_no_state_record() {
|
async fn can_get_user_with_no_state_record() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
let insert_result = db
|
let insert_result = db
|
||||||
.upsert_user(&User {
|
.upsert_user(&User {
|
||||||
username: "myuser".to_string(),
|
username: "myuser".to_string(),
|
||||||
|
@ -164,12 +170,13 @@ mod tests {
|
||||||
|
|
||||||
//These should be default values because the state record is missing.
|
//These should be default values because the state record is missing.
|
||||||
assert_eq!(user.active_room, None);
|
assert_eq!(user.active_room, None);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn can_insert_without_password() {
|
async fn can_insert_without_password() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
let insert_result = db
|
let insert_result = db
|
||||||
.upsert_user(&User {
|
.upsert_user(&User {
|
||||||
username: "myuser".to_string(),
|
username: "myuser".to_string(),
|
||||||
|
@ -189,12 +196,13 @@ mod tests {
|
||||||
let user = user.unwrap();
|
let user = user.unwrap();
|
||||||
assert_eq!(user.username, "myuser");
|
assert_eq!(user.username, "myuser");
|
||||||
assert_eq!(user.password, None);
|
assert_eq!(user.password, None);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn can_insert_without_active_room() {
|
async fn can_insert_without_active_room() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
let insert_result = db
|
let insert_result = db
|
||||||
.upsert_user(&User {
|
.upsert_user(&User {
|
||||||
username: "myuser".to_string(),
|
username: "myuser".to_string(),
|
||||||
|
@ -214,12 +222,13 @@ mod tests {
|
||||||
let user = user.unwrap();
|
let user = user.unwrap();
|
||||||
assert_eq!(user.username, "myuser");
|
assert_eq!(user.username, "myuser");
|
||||||
assert_eq!(user.active_room, None);
|
assert_eq!(user.active_room, None);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn can_update_user() {
|
async fn can_update_user() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
let insert_result1 = db
|
let insert_result1 = db
|
||||||
.upsert_user(&User {
|
.upsert_user(&User {
|
||||||
username: "myuser".to_string(),
|
username: "myuser".to_string(),
|
||||||
|
@ -254,12 +263,13 @@ mod tests {
|
||||||
assert_eq!(user.password, Some("123".to_string()));
|
assert_eq!(user.password, Some("123".to_string()));
|
||||||
assert_eq!(user.active_room, Some("room".to_string()));
|
assert_eq!(user.active_room, Some("room".to_string()));
|
||||||
assert_eq!(user.account_status, AccountStatus::AwaitingActivation);
|
assert_eq!(user.account_status, AccountStatus::AwaitingActivation);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn can_delete_user() {
|
async fn can_delete_user() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
let insert_result = db
|
let insert_result = db
|
||||||
.upsert_user(&User {
|
.upsert_user(&User {
|
||||||
username: "myuser".to_string(),
|
username: "myuser".to_string(),
|
||||||
|
@ -280,27 +290,32 @@ mod tests {
|
||||||
.expect("User retrieval query failed");
|
.expect("User retrieval query failed");
|
||||||
|
|
||||||
assert!(user.is_none());
|
assert!(user.is_none());
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn username_not_in_db_returns_none() {
|
async fn username_not_in_db_returns_none() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
let user = db
|
let user = db
|
||||||
.get_user("does not exist")
|
.get_user("does not exist")
|
||||||
.await
|
.await
|
||||||
.expect("Get user query failure");
|
.expect("Get user query failure");
|
||||||
|
|
||||||
assert!(user.is_none());
|
assert!(user.is_none());
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn authenticate_user_is_some_with_valid_password() {
|
async fn authenticate_user_is_some_with_valid_password() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
let insert_result = db
|
let insert_result = db
|
||||||
.upsert_user(&User {
|
.upsert_user(&User {
|
||||||
username: "myuser".to_string(),
|
username: "myuser".to_string(),
|
||||||
password: Some(crate::logic::hash_password("abc").expect("password hash error!")),
|
password: Some(
|
||||||
|
crate::logic::hash_password("abc").expect("password hash error!"),
|
||||||
|
),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -315,16 +330,19 @@ mod tests {
|
||||||
assert!(user.is_some());
|
assert!(user.is_some());
|
||||||
let user = user.unwrap();
|
let user = user.unwrap();
|
||||||
assert_eq!(user.username, "myuser");
|
assert_eq!(user.username, "myuser");
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn authenticate_user_is_none_with_wrong_password() {
|
async fn authenticate_user_is_none_with_wrong_password() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
let insert_result = db
|
let insert_result = db
|
||||||
.upsert_user(&User {
|
.upsert_user(&User {
|
||||||
username: "myuser".to_string(),
|
username: "myuser".to_string(),
|
||||||
password: Some(crate::logic::hash_password("abc").expect("password hash error!")),
|
password: Some(
|
||||||
|
crate::logic::hash_password("abc").expect("password hash error!"),
|
||||||
|
),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -337,5 +355,7 @@ mod tests {
|
||||||
.expect("User retrieval query failed");
|
.expect("User retrieval query failed");
|
||||||
|
|
||||||
assert!(user.is_none());
|
assert!(user.is_none());
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,22 +102,27 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::db::sqlite::Database;
|
use crate::db::sqlite::Database;
|
||||||
use crate::db::Variables;
|
use crate::db::Variables;
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
async fn create_db() -> Database {
|
async fn with_db<Fut>(f: impl FnOnce(Database) -> Fut)
|
||||||
|
where
|
||||||
|
Fut: Future<Output = ()>,
|
||||||
|
{
|
||||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||||
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
|
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Database::new(db_path.path().to_str().unwrap())
|
let db = Database::new(db_path.path().to_str().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap();
|
||||||
|
|
||||||
|
f(db).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn set_and_get_variable_test() {
|
async fn set_and_get_variable_test() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
db.set_user_variable("myuser", "myroom", "myvariable", 1)
|
db.set_user_variable("myuser", "myroom", "myvariable", 1)
|
||||||
.await
|
.await
|
||||||
.expect("Could not set variable");
|
.expect("Could not set variable");
|
||||||
|
@ -128,12 +133,13 @@ mod tests {
|
||||||
.expect("Could not get variable");
|
.expect("Could not get variable");
|
||||||
|
|
||||||
assert_eq!(value, 1);
|
assert_eq!(value, 1);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn get_missing_variable_test() {
|
async fn get_missing_variable_test() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
let value = db.get_user_variable("myuser", "myroom", "myvariable").await;
|
let value = db.get_user_variable("myuser", "myroom", "myvariable").await;
|
||||||
|
|
||||||
assert!(value.is_err());
|
assert!(value.is_err());
|
||||||
|
@ -141,12 +147,13 @@ mod tests {
|
||||||
value.err().unwrap(),
|
value.err().unwrap(),
|
||||||
DataError::KeyDoesNotExist(_)
|
DataError::KeyDoesNotExist(_)
|
||||||
));
|
));
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn get_other_user_variable_test() {
|
async fn get_other_user_variable_test() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
db.set_user_variable("myuser1", "myroom", "myvariable", 1)
|
db.set_user_variable("myuser1", "myroom", "myvariable", 1)
|
||||||
.await
|
.await
|
||||||
.expect("Could not set variable");
|
.expect("Could not set variable");
|
||||||
|
@ -160,12 +167,13 @@ mod tests {
|
||||||
value.err().unwrap(),
|
value.err().unwrap(),
|
||||||
DataError::KeyDoesNotExist(_)
|
DataError::KeyDoesNotExist(_)
|
||||||
));
|
));
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn count_variables_test() {
|
async fn count_variables_test() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
for variable_name in &["var1", "var2", "var3"] {
|
for variable_name in &["var1", "var2", "var3"] {
|
||||||
db.set_user_variable("myuser", "myroom", variable_name, 1)
|
db.set_user_variable("myuser", "myroom", variable_name, 1)
|
||||||
.await
|
.await
|
||||||
|
@ -178,12 +186,13 @@ mod tests {
|
||||||
.expect("Could not get count.");
|
.expect("Could not get count.");
|
||||||
|
|
||||||
assert_eq!(count, 3);
|
assert_eq!(count, 3);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn count_variables_respects_user_id() {
|
async fn count_variables_respects_user_id() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
for variable_name in &["var1", "var2", "var3"] {
|
for variable_name in &["var1", "var2", "var3"] {
|
||||||
db.set_user_variable("different-user", "myroom", variable_name, 1)
|
db.set_user_variable("different-user", "myroom", variable_name, 1)
|
||||||
.await
|
.await
|
||||||
|
@ -196,12 +205,13 @@ mod tests {
|
||||||
.expect("Could not get count.");
|
.expect("Could not get count.");
|
||||||
|
|
||||||
assert_eq!(count, 0);
|
assert_eq!(count, 0);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn count_variables_respects_room_id() {
|
async fn count_variables_respects_room_id() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
for variable_name in &["var1", "var2", "var3"] {
|
for variable_name in &["var1", "var2", "var3"] {
|
||||||
db.set_user_variable("myuser", "different-room", variable_name, 1)
|
db.set_user_variable("myuser", "different-room", variable_name, 1)
|
||||||
.await
|
.await
|
||||||
|
@ -214,12 +224,13 @@ mod tests {
|
||||||
.expect("Could not get count.");
|
.expect("Could not get count.");
|
||||||
|
|
||||||
assert_eq!(count, 0);
|
assert_eq!(count, 0);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn delete_variable_test() {
|
async fn delete_variable_test() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
for variable_name in &["var1", "var2", "var3"] {
|
for variable_name in &["var1", "var2", "var3"] {
|
||||||
db.set_user_variable("myuser", "myroom", variable_name, 1)
|
db.set_user_variable("myuser", "myroom", variable_name, 1)
|
||||||
.await
|
.await
|
||||||
|
@ -240,5 +251,7 @@ mod tests {
|
||||||
let var1 = db.get_user_variable("myuser", "myroom", "var1").await;
|
let var1 = db.get_user_variable("myuser", "myroom", "var1").await;
|
||||||
assert!(var1.is_err());
|
assert!(var1.is_err());
|
||||||
assert!(matches!(var1.err().unwrap(), DataError::KeyDoesNotExist(_)));
|
assert!(matches!(var1.err().unwrap(), DataError::KeyDoesNotExist(_)));
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,12 @@ pub enum BotError {
|
||||||
#[error("could not retrieve device id")]
|
#[error("could not retrieve device id")]
|
||||||
NoDeviceIdFound,
|
NoDeviceIdFound,
|
||||||
|
|
||||||
|
#[error("could not build client: {0}")]
|
||||||
|
ClientBuildError(#[from] matrix_sdk::ClientBuildError),
|
||||||
|
|
||||||
|
#[error("could not open matrix store: {0}")]
|
||||||
|
OpenStoreError(#[from] matrix_sdk::store::OpenStoreError),
|
||||||
|
|
||||||
#[error("command error: {0}")]
|
#[error("command error: {0}")]
|
||||||
CommandError(#[from] CommandError),
|
CommandError(#[from] CommandError),
|
||||||
|
|
||||||
|
@ -33,15 +39,15 @@ pub enum BotError {
|
||||||
#[error("could not parse URL")]
|
#[error("could not parse URL")]
|
||||||
UrlParseError(#[from] url::ParseError),
|
UrlParseError(#[from] url::ParseError),
|
||||||
|
|
||||||
|
#[error("could not parse ID")]
|
||||||
|
IdParseError(#[from] matrix_sdk::ruma::IdParseError),
|
||||||
|
|
||||||
#[error("error in matrix state store: {0}")]
|
#[error("error in matrix state store: {0}")]
|
||||||
MatrixStateStoreError(#[from] matrix_sdk::StoreError),
|
MatrixStateStoreError(#[from] matrix_sdk::StoreError),
|
||||||
|
|
||||||
#[error("uncategorized matrix SDK error: {0}")]
|
#[error("uncategorized matrix SDK error: {0}")]
|
||||||
MatrixError(#[from] matrix_sdk::Error),
|
MatrixError(#[from] matrix_sdk::Error),
|
||||||
|
|
||||||
#[error("uncategorized matrix SDK base error: {0}")]
|
|
||||||
MatrixBaseError(#[from] matrix_sdk::BaseError),
|
|
||||||
|
|
||||||
#[error("future canceled")]
|
#[error("future canceled")]
|
||||||
FutureCanceledError,
|
FutureCanceledError,
|
||||||
|
|
||||||
|
@ -79,8 +85,8 @@ pub enum BotError {
|
||||||
#[error("could not convert to proper integer type")]
|
#[error("could not convert to proper integer type")]
|
||||||
TryFromIntError(#[from] std::num::TryFromIntError),
|
TryFromIntError(#[from] std::num::TryFromIntError),
|
||||||
|
|
||||||
#[error("identifier error: {0}")]
|
// #[error("identifier error: {0}")]
|
||||||
IdentifierError(#[from] matrix_sdk::identifiers::Error),
|
// IdentifierError(#[from] matrix_sdk::ruma::Error),
|
||||||
|
|
||||||
#[error("password creation error: {0}")]
|
#[error("password creation error: {0}")]
|
||||||
PasswordCreationError(argon2::Error),
|
PasswordCreationError(argon2::Error),
|
||||||
|
|
|
@ -6,6 +6,9 @@ pub fn parse_help_topic(input: &str) -> Option<HelpTopic> {
|
||||||
"dicepool" => Some(HelpTopic::DicePool),
|
"dicepool" => Some(HelpTopic::DicePool),
|
||||||
"dice" => Some(HelpTopic::RollingDice),
|
"dice" => Some(HelpTopic::RollingDice),
|
||||||
"cthulhu" => Some(HelpTopic::Cthulhu),
|
"cthulhu" => Some(HelpTopic::Cthulhu),
|
||||||
|
"variables" => Some(HelpTopic::Variables),
|
||||||
|
"var" => Some(HelpTopic::Variables),
|
||||||
|
"variable" => Some(HelpTopic::Variables),
|
||||||
"" => Some(HelpTopic::General),
|
"" => Some(HelpTopic::General),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
@ -16,6 +19,7 @@ pub enum HelpTopic {
|
||||||
DicePool,
|
DicePool,
|
||||||
Cthulhu,
|
Cthulhu,
|
||||||
RollingDice,
|
RollingDice,
|
||||||
|
Variables,
|
||||||
General,
|
General,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +105,34 @@ Note: If !cthadv is given a variable, and the roll is successful, it will
|
||||||
update the variable with the new skill.
|
update the variable with the new skill.
|
||||||
"};
|
"};
|
||||||
|
|
||||||
|
const VARIABLES_HELP: &'static str = indoc! {"
|
||||||
|
Variables
|
||||||
|
|
||||||
|
Commands: !get, !set, !variables
|
||||||
|
|
||||||
|
Manage variables that can be substituted into roll commands.
|
||||||
|
|
||||||
|
Examples: !get myvar, !set myvar 10
|
||||||
|
|
||||||
|
!get <variable> = show variable of the given name
|
||||||
|
!set <variable> <num> = set a variable to a number
|
||||||
|
|
||||||
|
The !variables command will list all variables for the room. The
|
||||||
|
variables command cna be used in a secure room to avoid spamming the
|
||||||
|
actual room that the variable is set in.
|
||||||
|
|
||||||
|
Variable names can be used in all types of dice rolls:
|
||||||
|
|
||||||
|
!pool myvar + 3
|
||||||
|
!roll myvar
|
||||||
|
|
||||||
|
There are some limitations on variables: they cannot themselves be
|
||||||
|
dice expressions (i.e. can only be numbers), and they must be uniquely
|
||||||
|
parseable in an expression (i.e 'myvard6' does not work for the !roll
|
||||||
|
command).
|
||||||
|
|
||||||
|
"};
|
||||||
|
|
||||||
const GENERAL_HELP: &'static str = indoc! {"
|
const GENERAL_HELP: &'static str = indoc! {"
|
||||||
General Help
|
General Help
|
||||||
|
|
||||||
|
@ -117,6 +149,7 @@ impl HelpTopic {
|
||||||
HelpTopic::DicePool => DICEPOOL_HELP,
|
HelpTopic::DicePool => DICEPOOL_HELP,
|
||||||
HelpTopic::Cthulhu => CTHULHU_HELP,
|
HelpTopic::Cthulhu => CTHULHU_HELP,
|
||||||
HelpTopic::RollingDice => DICE_HELP,
|
HelpTopic::RollingDice => DICE_HELP,
|
||||||
|
HelpTopic::Variables => VARIABLES_HELP,
|
||||||
HelpTopic::General => GENERAL_HELP,
|
HelpTopic::General => GENERAL_HELP,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,3 +14,4 @@ pub mod models;
|
||||||
mod parser;
|
mod parser;
|
||||||
pub mod rpc;
|
pub mod rpc;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
pub mod systems;
|
||||||
|
|
|
@ -71,22 +71,27 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::db::Users;
|
use crate::db::Users;
|
||||||
use crate::models::{AccountStatus, User};
|
use crate::models::{AccountStatus, User};
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
async fn create_db() -> Database {
|
async fn with_db<Fut>(f: impl FnOnce(Database) -> Fut)
|
||||||
|
where
|
||||||
|
Fut: Future<Output = ()>,
|
||||||
|
{
|
||||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||||
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
|
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Database::new(db_path.path().to_str().unwrap())
|
let db = Database::new(db_path.path().to_str().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap();
|
||||||
|
|
||||||
|
f(db).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn get_account_no_user_exists() {
|
async fn get_account_no_user_exists() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
let account = get_account(&db, "@test:example.com")
|
let account = get_account(&db, "@test:example.com")
|
||||||
.await
|
.await
|
||||||
.expect("Account retrieval didn't work");
|
.expect("Account retrieval didn't work");
|
||||||
|
@ -95,12 +100,13 @@ mod tests {
|
||||||
|
|
||||||
let user = account.transient_user().unwrap();
|
let user = account.transient_user().unwrap();
|
||||||
assert_eq!(user.username, "@test:example.com");
|
assert_eq!(user.username, "@test:example.com");
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
async fn get_or_create_user_when_user_exists() {
|
async fn get_or_create_user_when_user_exists() {
|
||||||
let db = create_db().await;
|
with_db(|db| async move {
|
||||||
|
|
||||||
let user = User {
|
let user = User {
|
||||||
username: "myuser".to_string(),
|
username: "myuser".to_string(),
|
||||||
password: Some("abc".to_string()),
|
password: Some("abc".to_string()),
|
||||||
|
@ -119,5 +125,7 @@ mod tests {
|
||||||
|
|
||||||
let user_again = account.registered_user().unwrap();
|
let user_again = account.registered_user().unwrap();
|
||||||
assert_eq!(user, *user_again);
|
assert_eq!(user, *user_again);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,12 @@ use std::path::PathBuf;
|
||||||
|
|
||||||
use futures::stream::{self, StreamExt, TryStreamExt};
|
use futures::stream::{self, StreamExt, TryStreamExt};
|
||||||
use log::error;
|
use log::error;
|
||||||
use matrix_sdk::{events::room::message::NoticeMessageEventContent, room::Joined, ClientConfig};
|
use matrix_sdk::ruma::events::room::message::{InReplyTo, RoomMessageEventContent, Relation};
|
||||||
use matrix_sdk::{
|
use matrix_sdk::ruma::events::AnyMessageLikeEventContent;
|
||||||
events::room::message::{InReplyTo, Relation},
|
use matrix_sdk::ruma::{RoomId, OwnedEventId, OwnedUserId};
|
||||||
events::room::message::{MessageEventContent, MessageType},
|
use matrix_sdk::Client;
|
||||||
events::AnyMessageEventContent,
|
use matrix_sdk::Error as MatrixError;
|
||||||
identifiers::EventId,
|
use matrix_sdk::room::Joined;
|
||||||
Error as MatrixError,
|
|
||||||
};
|
|
||||||
use matrix_sdk::{identifiers::RoomId, identifiers::UserId, Client};
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{config::Config, error::BotError};
|
use crate::{config::Config, error::BotError};
|
||||||
|
@ -32,12 +29,16 @@ fn extract_error_message(error: MatrixError) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the matrix client.
|
/// Creates the matrix client.
|
||||||
pub fn create_client(config: &Config) -> Result<Client, BotError> {
|
pub async fn create_client(config: &Config) -> Result<Client, BotError> {
|
||||||
let cache_dir = cache_dir()?;
|
let cache_dir = cache_dir()?;
|
||||||
let client_config = ClientConfig::new().store_path(cache_dir);
|
|
||||||
let homeserver_url = Url::parse(&config.matrix_homeserver())?;
|
let homeserver_url = Url::parse(&config.matrix_homeserver())?;
|
||||||
|
|
||||||
Ok(Client::new_with_config(homeserver_url, client_config)?)
|
let client = Client::builder()
|
||||||
|
.sled_store(cache_dir, None)?
|
||||||
|
.homeserver_url(homeserver_url).build()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve a list of users in a given room.
|
/// Retrieve a list of users in a given room.
|
||||||
|
@ -59,7 +60,7 @@ pub async fn get_users_in_room(
|
||||||
|
|
||||||
pub async fn get_rooms_for_user(
|
pub async fn get_rooms_for_user(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
user: &UserId,
|
user: &OwnedUserId,
|
||||||
) -> Result<Vec<Joined>, MatrixError> {
|
) -> Result<Vec<Joined>, MatrixError> {
|
||||||
// Carries errors through, in case we cannot load joined user IDs
|
// Carries errors through, in case we cannot load joined user IDs
|
||||||
// from the room for some reason.
|
// from the room for some reason.
|
||||||
|
@ -87,7 +88,7 @@ pub async fn send_message(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
message: (&str, &str),
|
message: (&str, &str),
|
||||||
reply_to: Option<EventId>,
|
reply_to: Option<OwnedEventId>,
|
||||||
) {
|
) {
|
||||||
let (html, plain) = message;
|
let (html, plain) = message;
|
||||||
let room = match client.get_joined_room(room_id) {
|
let room = match client.get_joined_room(room_id) {
|
||||||
|
@ -95,15 +96,13 @@ pub async fn send_message(
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut content = MessageEventContent::new(MessageType::Notice(
|
let mut content = RoomMessageEventContent::notice_html(plain.trim(), html);
|
||||||
NoticeMessageEventContent::html(plain.trim(), html),
|
|
||||||
));
|
|
||||||
|
|
||||||
content.relates_to = reply_to.map(|event_id| Relation::Reply {
|
content.relates_to = reply_to.map(|event_id| Relation::Reply {
|
||||||
in_reply_to: InReplyTo::new(event_id),
|
in_reply_to: InReplyTo::new(event_id)
|
||||||
});
|
});
|
||||||
|
|
||||||
let content = AnyMessageEventContent::RoomMessage(content);
|
let content = AnyMessageLikeEventContent::RoomMessage(content);
|
||||||
|
|
||||||
let result = room.send(content, None).await;
|
let result = room.send(content, None).await;
|
||||||
|
|
||||||
|
|
|
@ -151,8 +151,9 @@ where
|
||||||
/// should not have an operator, but every one after that should.
|
/// should not have an operator, but every one after that should.
|
||||||
/// Accepts expressions like "8", "10 + variablename", "variablename -
|
/// Accepts expressions like "8", "10 + variablename", "variablename -
|
||||||
/// 3", etc. This function is currently common to systems that don't
|
/// 3", etc. This function is currently common to systems that don't
|
||||||
/// deal with XdY rolls. Support for that will be added later.
|
/// deal with XdY rolls. Support for that will be added later. Returns
|
||||||
pub fn parse_amounts(input: &str) -> ParseResult<Vec<Amount>> {
|
/// parsed amounts and unconsumed input (e.g. roll modifiers).
|
||||||
|
pub fn parse_amounts(input: &str) -> ParseResult<(Vec<Amount>, &str)> {
|
||||||
let input = input.trim();
|
let input = input.trim();
|
||||||
|
|
||||||
let remaining_amounts = many(amount_parser()).map(|amounts: Vec<ParseResult<Amount>>| amounts);
|
let remaining_amounts = many(amount_parser()).map(|amounts: Vec<ParseResult<Amount>>| amounts);
|
||||||
|
@ -169,31 +170,23 @@ pub fn parse_amounts(input: &str) -> ParseResult<Vec<Amount>> {
|
||||||
(amounts, results.1)
|
(amounts, results.1)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if rest.len() == 0 {
|
|
||||||
// Any ParseResult errors will short-circuit the collect.
|
// Any ParseResult errors will short-circuit the collect.
|
||||||
results.into_iter().collect()
|
let results: Vec<Amount> = results.into_iter().collect::<ParseResult<_>>()?;
|
||||||
} else {
|
Ok((results, rest))
|
||||||
Err(DiceParsingError::UnconsumedInput)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an expression that expects a single number or variable. No
|
/// Parse an expression that expects a single number or variable. No
|
||||||
/// operators are allowed. This function is common to systems that
|
/// operators are allowed. This function is common to systems that
|
||||||
/// don't deal with XdY rolls. Currently. this function does not
|
/// don't deal with XdY rolls. Currently. this function does not
|
||||||
/// support parsing negative numbers.
|
/// support parsing negative numbers. Returns the parsed amount and
|
||||||
pub fn parse_single_amount(input: &str) -> ParseResult<Amount> {
|
/// any unconsumed input (useful for dice roll modifiers).
|
||||||
|
pub fn parse_single_amount(input: &str) -> ParseResult<(Amount, &str)> {
|
||||||
// TODO add support for negative numbers, as technically they
|
// TODO add support for negative numbers, as technically they
|
||||||
// should be allowed.
|
// should be allowed.
|
||||||
let input = input.trim();
|
let input = input.trim();
|
||||||
let mut parser = first_amount_parser().map(|amount: ParseResult<Amount>| amount);
|
let mut parser = first_amount_parser().map(|amount: ParseResult<Amount>| amount);
|
||||||
|
|
||||||
let (result, rest) = parser.parse(input)?;
|
let (result, rest) = parser.parse(input)?;
|
||||||
|
Ok((result?, rest))
|
||||||
if rest.len() == 0 {
|
|
||||||
result
|
|
||||||
} else {
|
|
||||||
Err(DiceParsingError::UnconsumedInput)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -206,10 +199,13 @@ mod parse_single_amount_tests {
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap(),
|
result.unwrap(),
|
||||||
|
(
|
||||||
Amount {
|
Amount {
|
||||||
operator: Operator::Plus,
|
operator: Operator::Plus,
|
||||||
element: Element::Variable("abc".to_string())
|
element: Element::Variable("abc".to_string())
|
||||||
}
|
},
|
||||||
|
""
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,23 +229,14 @@ mod parse_single_amount_tests {
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap(),
|
result.unwrap(),
|
||||||
|
(
|
||||||
Amount {
|
Amount {
|
||||||
operator: Operator::Plus,
|
operator: Operator::Plus,
|
||||||
element: Element::Number(1)
|
element: Element::Number(1)
|
||||||
}
|
},
|
||||||
|
""
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_multiple_elements_test() {
|
|
||||||
let result = parse_single_amount("1+abc");
|
|
||||||
assert!(result.is_err());
|
|
||||||
|
|
||||||
let result = parse_single_amount("abc+1");
|
|
||||||
assert!(result.is_err());
|
|
||||||
|
|
||||||
let result = parse_single_amount("-1-abc");
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,20 +250,26 @@ mod parse_many_amounts_tests {
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap(),
|
result.unwrap(),
|
||||||
|
(
|
||||||
vec![Amount {
|
vec![Amount {
|
||||||
operator: Operator::Plus,
|
operator: Operator::Plus,
|
||||||
element: Element::Number(1)
|
element: Element::Number(1)
|
||||||
}]
|
}],
|
||||||
|
""
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = parse_amounts("10");
|
let result = parse_amounts("10");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap(),
|
result.unwrap(),
|
||||||
|
(
|
||||||
vec![Amount {
|
vec![Amount {
|
||||||
operator: Operator::Plus,
|
operator: Operator::Plus,
|
||||||
element: Element::Number(10)
|
element: Element::Number(10)
|
||||||
}]
|
}],
|
||||||
|
""
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,20 +288,26 @@ mod parse_many_amounts_tests {
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap(),
|
result.unwrap(),
|
||||||
|
(
|
||||||
vec![Amount {
|
vec![Amount {
|
||||||
operator: Operator::Plus,
|
operator: Operator::Plus,
|
||||||
element: Element::Variable("asdf".to_string())
|
element: Element::Variable("asdf".to_string())
|
||||||
}]
|
}],
|
||||||
|
""
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = parse_amounts("nosis");
|
let result = parse_amounts("nosis");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap(),
|
result.unwrap(),
|
||||||
|
(
|
||||||
vec![Amount {
|
vec![Amount {
|
||||||
operator: Operator::Plus,
|
operator: Operator::Plus,
|
||||||
element: Element::Variable("nosis".to_string())
|
element: Element::Variable("nosis".to_string())
|
||||||
}]
|
}],
|
||||||
|
""
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,8 @@ use crate::matrix;
|
||||||
use crate::{config::Config, db::sqlite::Database};
|
use crate::{config::Config, db::sqlite::Database};
|
||||||
use futures::stream;
|
use futures::stream;
|
||||||
use futures::{StreamExt, TryFutureExt, TryStreamExt};
|
use futures::{StreamExt, TryFutureExt, TryStreamExt};
|
||||||
use matrix_sdk::{identifiers::UserId, room::Joined, Client};
|
use matrix_sdk::ruma::OwnedUserId;
|
||||||
|
use matrix_sdk::{room::Joined, Client};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tenebrous_rpc::protos::dicebot::{
|
use tenebrous_rpc::protos::dicebot::{
|
||||||
|
@ -63,12 +64,7 @@ impl Dicebot for DicebotRpcService {
|
||||||
.get_user_variable(&request.user_id, &request.room_id, &request.variable_name)
|
.get_user_variable(&request.user_id, &request.room_id, &request.variable_name)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Response::new(GetVariableReply {
|
Ok(Response::new(GetVariableReply { value }))
|
||||||
user_id: request.user_id.clone(),
|
|
||||||
room_id: request.room_id.clone(),
|
|
||||||
variable_name: request.variable_name.clone(),
|
|
||||||
value,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_all_variables(
|
async fn get_all_variables(
|
||||||
|
@ -81,11 +77,7 @@ impl Dicebot for DicebotRpcService {
|
||||||
.get_user_variables(&request.user_id, &request.room_id)
|
.get_user_variables(&request.user_id, &request.room_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Response::new(GetAllVariablesReply {
|
Ok(Response::new(GetAllVariablesReply { variables }))
|
||||||
user_id: request.user_id.clone(),
|
|
||||||
room_id: request.user_id.clone(),
|
|
||||||
variables,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn rooms_for_user(
|
async fn rooms_for_user(
|
||||||
|
@ -93,7 +85,7 @@ impl Dicebot for DicebotRpcService {
|
||||||
request: Request<UserIdRequest>,
|
request: Request<UserIdRequest>,
|
||||||
) -> Result<Response<RoomsListReply>, Status> {
|
) -> Result<Response<RoomsListReply>, Status> {
|
||||||
let UserIdRequest { user_id } = request.into_inner();
|
let UserIdRequest { user_id } = request.into_inner();
|
||||||
let user_id = UserId::try_from(user_id).map_err(BotError::from)?;
|
let user_id = OwnedUserId::try_from(user_id).map_err(BotError::from)?;
|
||||||
|
|
||||||
let rooms_for_user = matrix::get_rooms_for_user(&self.client, &user_id)
|
let rooms_for_user = matrix::get_rooms_for_user(&self.client, &user_id)
|
||||||
.err_into::<BotError>()
|
.err_into::<BotError>()
|
||||||
|
@ -103,7 +95,7 @@ impl Dicebot for DicebotRpcService {
|
||||||
.filter_map(|room: Joined| async move {
|
.filter_map(|room: Joined| async move {
|
||||||
let room: Result<Room, _> = room.display_name().await.map(|room_name| Room {
|
let room: Result<Room, _> = room.display_name().await.map(|room_name| Room {
|
||||||
room_id: room.room_id().to_string(),
|
room_id: room.room_id().to_string(),
|
||||||
display_name: room_name,
|
display_name: room_name.to_string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Some(room)
|
Some(room)
|
||||||
|
@ -120,9 +112,6 @@ impl Dicebot for DicebotRpcService {
|
||||||
|
|
||||||
rooms.sort_by(sort);
|
rooms.sort_by(sort);
|
||||||
|
|
||||||
Ok(Response::new(RoomsListReply {
|
Ok(Response::new(RoomsListReply { rooms }))
|
||||||
user_id: user_id.into_string(),
|
|
||||||
rooms,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
use strum::{AsRefStr, Display, EnumIter, EnumString};
|
||||||
|
|
||||||
|
#[derive(EnumString, EnumIter, AsRefStr, Display)]
|
||||||
|
pub(crate) enum GameSystem {
|
||||||
|
ChroniclesOfDarkness,
|
||||||
|
Changeling,
|
||||||
|
MageTheAwakening,
|
||||||
|
WerewolfTheForsaken,
|
||||||
|
DeviantTheRenegades,
|
||||||
|
MummyTheCurse,
|
||||||
|
PrometheanTheCreated,
|
||||||
|
CallOfCthulhu,
|
||||||
|
DungeonsAndDragons5e,
|
||||||
|
DungeonsAndDragons4e,
|
||||||
|
DungeonsAndDragons35e,
|
||||||
|
DungeonsAndDragons2e,
|
||||||
|
DungeonsAndDragons1e,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameSystem {}
|
|
@ -3,18 +3,16 @@ name = "tenebrous-rpc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["projectmoon <projectmoon@agnos.is>"]
|
authors = ["projectmoon <projectmoon@agnos.is>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
description = "gRPC protobuf models for Tenebrous."
|
||||||
|
homepage = "https://git.agnos.is/projectmoon/tenebrous-dicebot"
|
||||||
|
repository = "https://git.agnos.is/projectmoon/tenebrous-dicebot"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
# Default is to build tonic and tonic-build as they normally are. The
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
# wasm feature is for WebAssmebly, and disables Tonic's transport
|
|
||||||
# feature. There is a separate grpc-web-client that can use tonic's
|
|
||||||
# requests to make grpc-web requests.
|
|
||||||
[features]
|
|
||||||
default = ["tonic/default", "tonic-build/default"]
|
|
||||||
wasm = [ "tonic/codegen", "tonic/prost", "tonic-build/prost"]
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tonic-build = { version = "0.4", default_features = false }
|
tonic-build = "0.4"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tonic = { version = "0.4", default_features = false }
|
tonic = "0.4"
|
||||||
prost = "0.7"
|
prost = "0.7"
|
14
rpc/build.rs
14
rpc/build.rs
|
@ -1,16 +1,4 @@
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if cfg!(feature = "only-client") {
|
tonic_build::compile_protos("protos/dicebot.proto")?;
|
||||||
tonic_build::configure().build_server(false).compile(
|
|
||||||
&["protos/dicebot.proto", "protos/web-api.proto"],
|
|
||||||
&["protos/"],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
tonic_build::configure().compile(
|
|
||||||
&["protos/dicebot.proto", "protos/web-api.proto"],
|
|
||||||
&["protos/"],
|
|
||||||
)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -15,10 +15,7 @@ message GetVariableRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetVariableReply {
|
message GetVariableReply {
|
||||||
string user_id = 1;
|
int32 value = 1;
|
||||||
string room_id = 2;
|
|
||||||
string variable_name = 3;
|
|
||||||
int32 value = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetAllVariablesRequest {
|
message GetAllVariablesRequest {
|
||||||
|
@ -27,9 +24,7 @@ message GetAllVariablesRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetAllVariablesReply {
|
message GetAllVariablesReply {
|
||||||
string user_id = 1;
|
map<string, int32> variables = 1;
|
||||||
string room_id = 2;
|
|
||||||
map<string, int32> variables = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message SetVariableRequest {
|
message SetVariableRequest {
|
||||||
|
@ -53,6 +48,5 @@ message RoomsListReply {
|
||||||
string display_name = 2;
|
string display_name = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
string user_id = 1;
|
repeated Room rooms = 1;
|
||||||
repeated Room rooms = 2;
|
|
||||||
}
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
syntax = "proto3";
|
|
||||||
package web_api;
|
|
||||||
|
|
||||||
service WebApi {
|
|
||||||
rpc ListRoom(UserIdRequest) returns (RoomsListReply);
|
|
||||||
}
|
|
||||||
|
|
||||||
message UserIdRequest {
|
|
||||||
string user_id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RoomsListReply {
|
|
||||||
message Room {
|
|
||||||
string room_id = 1;
|
|
||||||
string display_name = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
repeated Room rooms = 1;
|
|
||||||
}
|
|
|
@ -1,28 +1,5 @@
|
||||||
pub mod protos {
|
pub mod protos {
|
||||||
pub mod web_api {
|
|
||||||
tonic::include_proto!("web_api");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod dicebot {
|
pub mod dicebot {
|
||||||
tonic::include_proto!("dicebot");
|
tonic::include_proto!("dicebot");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use protos::dicebot::dicebot_client::DicebotClient;
|
|
||||||
use tonic::{metadata::MetadataValue, transport::Channel as TonicChannel, Request as TonicRequest};
|
|
||||||
|
|
||||||
#[cfg(feature = "default")]
|
|
||||||
pub async fn create_client(
|
|
||||||
address: &'static str,
|
|
||||||
shared_secret: &str,
|
|
||||||
) -> Result<DicebotClient<TonicChannel>, Box<dyn std::error::Error>> {
|
|
||||||
let channel = TonicChannel::from_shared(address)?.connect().await?;
|
|
||||||
|
|
||||||
let bearer = MetadataValue::from_str(&format!("Bearer {}", shared_secret))?;
|
|
||||||
let client = DicebotClient::with_interceptor(channel, move |mut req: TonicRequest<()>| {
|
|
||||||
req.metadata_mut().insert("authorization", bearer.clone());
|
|
||||||
Ok(req)
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(client)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
fn main() {
|
|
||||||
println!("Hello, world!");
|
|
||||||
}
|
|
|
@ -1,691 +0,0 @@
|
||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
[[package]]
|
|
||||||
name = "anyhow"
|
|
||||||
version = "1.0.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anymap"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arrayvec"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "autocfg"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bincode"
|
|
||||||
version = "1.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d"
|
|
||||||
dependencies = [
|
|
||||||
"byteorder",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitflags"
|
|
||||||
version = "1.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "boolinator"
|
|
||||||
version = "2.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bumpalo"
|
|
||||||
version = "3.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "byteorder"
|
|
||||||
version = "1.3.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytes"
|
|
||||||
version = "0.5.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "0.1.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-match"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8100e46ff92eb85bf6dc2930c73f2a4f7176393c84a9446b3d501e1b354e7b34"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "console_error_panic_hook"
|
|
||||||
version = "0.1.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if 0.1.10",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fnv"
|
|
||||||
version = "1.0.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures"
|
|
||||||
version = "0.3.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-executor",
|
|
||||||
"futures-io",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-channel"
|
|
||||||
version = "0.3.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5"
|
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
"futures-sink",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-core"
|
|
||||||
version = "0.3.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-executor"
|
|
||||||
version = "0.3.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314"
|
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-io"
|
|
||||||
version = "0.3.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-macro"
|
|
||||||
version = "0.3.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro-hack",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-sink"
|
|
||||||
version = "0.3.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-task"
|
|
||||||
version = "0.3.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-util"
|
|
||||||
version = "0.3.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-io",
|
|
||||||
"futures-macro",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-task",
|
|
||||||
"memchr",
|
|
||||||
"pin-project",
|
|
||||||
"pin-utils",
|
|
||||||
"proc-macro-hack",
|
|
||||||
"proc-macro-nested",
|
|
||||||
"slab",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "68ce6f2dfa9f57f15b848efa2aade5e1850dc72986b87a2b0752d44ca08f4967"
|
|
||||||
dependencies = [
|
|
||||||
"gloo-console-timer",
|
|
||||||
"gloo-events",
|
|
||||||
"gloo-file",
|
|
||||||
"gloo-timers",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-console-timer"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b48675544b29ac03402c6dffc31a912f716e38d19f7e74b78b7e900ec3c941ea"
|
|
||||||
dependencies = [
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-events"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "088514ec8ef284891c762c88a66b639b3a730134714692ee31829765c5bc814f"
|
|
||||||
dependencies = [
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-file"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f9fecfe46b5dc3cc46f58e98ba580cc714f2c93860796d002eb3527a465ef49"
|
|
||||||
dependencies = [
|
|
||||||
"gloo-events",
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-timers"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "34f595585f103464d8d2f6e9864682d74c1601fed5e07d62b1c9058dba8246fb"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "http"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"fnv",
|
|
||||||
"itoa",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indexmap"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5b88cd59ee5f71fea89a62248fc8f387d44400cefe05ef548466d61ced9029a7"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"hashbrown",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itoa"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "js-sys"
|
|
||||||
version = "0.3.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "85a7e2c92a4804dd459b86c339278d0fe87cf93757fae222c3fa3ae75458bc73"
|
|
||||||
dependencies = [
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazy_static"
|
|
||||||
version = "1.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lexical-core"
|
|
||||||
version = "0.7.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
|
|
||||||
dependencies = [
|
|
||||||
"arrayvec",
|
|
||||||
"bitflags",
|
|
||||||
"cfg-if 1.0.0",
|
|
||||||
"ryu",
|
|
||||||
"static_assertions",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "log"
|
|
||||||
version = "0.4.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if 0.1.10",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "memchr"
|
|
||||||
version = "2.3.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "5.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
|
|
||||||
dependencies = [
|
|
||||||
"lexical-core",
|
|
||||||
"memchr",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell"
|
|
||||||
version = "1.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-project"
|
|
||||||
version = "0.4.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa"
|
|
||||||
dependencies = [
|
|
||||||
"pin-project-internal",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-project-internal"
|
|
||||||
version = "0.4.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-utils"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro-hack"
|
|
||||||
version = "0.5.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro-nested"
|
|
||||||
version = "0.1.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-xid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "1.0.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ryu"
|
|
||||||
version = "1.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde"
|
|
||||||
version = "1.0.114"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3"
|
|
||||||
dependencies = [
|
|
||||||
"serde_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_derive"
|
|
||||||
version = "1.0.114"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_json"
|
|
||||||
version = "1.0.57"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c"
|
|
||||||
dependencies = [
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "slab"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "static_assertions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "1.0.36"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4cdb98bcb1f9d81d07b536179c269ea15999b5d14ea958196413869445bb5250"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-xid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "1.0.20"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "1.0.20"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-xid"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version_check"
|
|
||||||
version = "0.9.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen"
|
|
||||||
version = "0.2.67"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f0563a9a4b071746dd5aedbc3a28c6fe9be4586fb3fbadb67c400d4f53c6b16c"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if 0.1.10",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"wasm-bindgen-macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-backend"
|
|
||||||
version = "0.2.67"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bc71e4c5efa60fb9e74160e89b93353bc24059999c0ae0fb03affc39770310b0"
|
|
||||||
dependencies = [
|
|
||||||
"bumpalo",
|
|
||||||
"lazy_static",
|
|
||||||
"log",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
"wasm-bindgen-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-futures"
|
|
||||||
version = "0.4.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "95f8d235a77f880bcef268d379810ea6c0af2eacfa90b1ad5af731776e0c4699"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if 0.1.10",
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-macro"
|
|
||||||
version = "0.2.67"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "97c57cefa5fa80e2ba15641578b44d36e7a64279bc5ed43c6dbaf329457a2ed2"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"wasm-bindgen-macro-support",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-macro-support"
|
|
||||||
version = "0.2.67"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "841a6d1c35c6f596ccea1f82504a192a60378f64b3bb0261904ad8f2f5657556"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
"wasm-bindgen-backend",
|
|
||||||
"wasm-bindgen-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-shared"
|
|
||||||
version = "0.2.67"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "93b162580e34310e5931c4b792560108b10fd14d64915d7fff8ff00180e70092"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "web-sys"
|
|
||||||
version = "0.3.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dda38f4e5ca63eda02c059d243aa25b5f35ab98451e518c51612cd0f1bd19a47"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yew"
|
|
||||||
version = "0.17.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f0d82d3fb2ab53913967b0a50a5a693ab75e1465cb091a24519a9441246c2627"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"anymap",
|
|
||||||
"bincode",
|
|
||||||
"cfg-if 0.1.10",
|
|
||||||
"cfg-match",
|
|
||||||
"console_error_panic_hook",
|
|
||||||
"futures",
|
|
||||||
"gloo",
|
|
||||||
"http",
|
|
||||||
"indexmap",
|
|
||||||
"js-sys",
|
|
||||||
"log",
|
|
||||||
"proc-macro-hack",
|
|
||||||
"proc-macro-nested",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"slab",
|
|
||||||
"thiserror",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
"yew-macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yew-app"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"wasm-bindgen",
|
|
||||||
"yew",
|
|
||||||
"yew-router",
|
|
||||||
"yewdux",
|
|
||||||
"yewtil",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yew-macro"
|
|
||||||
version = "0.17.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "61a9a452e63b6222b28b426dafbc6b207192e0127cdb93324cc7407b8c7e1768"
|
|
||||||
dependencies = [
|
|
||||||
"boolinator",
|
|
||||||
"lazy_static",
|
|
||||||
"proc-macro-hack",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yew-router"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "20f46279cd28a50e0e9271352ce6d7be7f10e6ba449c76544985e64222f5e999"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if 0.1.10",
|
|
||||||
"cfg-match",
|
|
||||||
"gloo",
|
|
||||||
"js-sys",
|
|
||||||
"log",
|
|
||||||
"nom",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
"yew",
|
|
||||||
"yew-router-macro",
|
|
||||||
"yew-router-route-parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yew-router-macro"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "768693f16c930d8a8742c2e5f62f258d5a6f0392e8c9265a45c550fac4cbe5a7"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
"yew-router-route-parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yew-router-route-parser"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "49bf2f66f0e61d65d1637523fa1faedd5aa291cf9cb8d8fb200678472279b672"
|
|
||||||
dependencies = [
|
|
||||||
"nom",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yewdux"
|
|
||||||
version = "0.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "223ffe9ac0d8ef83429ed639fc2cd159d747080eacbc1d8bc4955d37757509d1"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"yew",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yewtil"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "003f4f533ee696c1f69d753db2c66096dec5700a93bd0bc801ab5891fec041c6"
|
|
||||||
dependencies = [
|
|
||||||
"futures",
|
|
||||||
"log",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"yew",
|
|
||||||
]
|
|
|
@ -1,47 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "tenebrous-web-ui"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["projectmoon <projectmoon@agnos.is>"]
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib", "rlib"]
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
tenebrous-api = { path = "../../api" }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
yew = { version = "0.18" }
|
|
||||||
yewtil = {version = "0.4" }
|
|
||||||
yew-router = {version = "0.15" }
|
|
||||||
yewdux = { git = "https://github.com/intendednull/yewdux", rev = "v0.6.2"}
|
|
||||||
wasm-bindgen = { version = "0.2" }
|
|
||||||
wasm-bindgen-futures = "0.4"
|
|
||||||
js-sys = "0.3"
|
|
||||||
graphql_client = { git = "https://github.com/graphql-rust/graphql-client", branch = "master" }
|
|
||||||
graphql_client_web = { git = "https://github.com/graphql-rust/graphql-client", branch = "master" }
|
|
||||||
serde = { version = "1.0.67", features = ["derive"] }
|
|
||||||
serde_json = {version = "1.0" }
|
|
||||||
thiserror = "1.0"
|
|
||||||
jsonwebtoken = "7.2"
|
|
||||||
|
|
||||||
[dependencies.web-sys]
|
|
||||||
version = "0.3"
|
|
||||||
features = [
|
|
||||||
'Headers',
|
|
||||||
'Request',
|
|
||||||
'RequestInit',
|
|
||||||
'RequestMode',
|
|
||||||
'Response',
|
|
||||||
'Window',
|
|
||||||
]
|
|
||||||
|
|
||||||
# hopefully we can add grpc-web later instead of graphql.
|
|
||||||
# prost = { version = "0.7.0", default-features = false }
|
|
||||||
# tonic = { git = "https://github.com/hyperium/tonic", branch = "master", default-features = false, features = ["codegen", "prost"] }
|
|
||||||
# tenebrous-rpc = { path = "../../rpc", default_features = false, features = ["wasm"] }
|
|
||||||
|
|
||||||
# [build-dependencies]
|
|
||||||
# tonic-build = { version = "0.4", default-features = false, features = ["prost"] }
|
|
|
@ -1,7 +0,0 @@
|
||||||
use std::fs;
|
|
||||||
use tenebrous_api::schema;
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let schema_doc = schema::schema().as_schema_language();
|
|
||||||
fs::write("schema.graphql", schema_doc)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
query GetUserVariable($roomId: String!, $userId: String!, $variable: String!) {
|
|
||||||
variable(roomId: $roomId, userId: $userId, variable: $variable) {
|
|
||||||
roomId
|
|
||||||
userId
|
|
||||||
value
|
|
||||||
variableName
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
query RoomsForUser($userId: String!) {
|
|
||||||
userRooms(userId: $userId) {
|
|
||||||
userId
|
|
||||||
rooms {
|
|
||||||
roomId
|
|
||||||
displayName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
"User variable in a room."
|
|
||||||
type UserVariable {
|
|
||||||
roomId: String!
|
|
||||||
userId: String!
|
|
||||||
variableName: String!
|
|
||||||
value: Int!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
apiVersion: String!
|
|
||||||
variable(roomId: String!, userId: String!, variable: String!): UserVariable!
|
|
||||||
userRooms(userId: String!): UserRoomList!
|
|
||||||
}
|
|
||||||
|
|
||||||
"List of rooms a user is in."
|
|
||||||
type UserRoomList {
|
|
||||||
userId: String!
|
|
||||||
rooms: [Room!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
"A matrix room."
|
|
||||||
type Room {
|
|
||||||
roomId: String!
|
|
||||||
displayName: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
schema {
|
|
||||||
query: Query
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
"User variable in a room."
|
|
||||||
type UserVariable {
|
|
||||||
roomId: String!
|
|
||||||
userId: String!
|
|
||||||
variableName: String!
|
|
||||||
value: Int!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
apiVersion: String!
|
|
||||||
variable(roomId: String!, userId: String!, variable: String!): UserVariable!
|
|
||||||
userRooms(userId: String!): UserRoomList!
|
|
||||||
}
|
|
||||||
|
|
||||||
"List of rooms a user is in."
|
|
||||||
type UserRoomList {
|
|
||||||
userId: String!
|
|
||||||
rooms: [Room!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
"A matrix room."
|
|
||||||
type Room {
|
|
||||||
roomId: String!
|
|
||||||
displayName: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
schema {
|
|
||||||
query: Query
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
use crate::error::UiError;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use wasm_bindgen::prelude::*;
|
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
use wasm_bindgen_futures::JsFuture;
|
|
||||||
use web_sys::{console, Request, RequestCredentials, RequestInit, RequestMode, Response};
|
|
||||||
|
|
||||||
/// A struct representing an error coming back from the REST API
|
|
||||||
/// endpoint. The API server encodes any errors as JSON objects with a
|
|
||||||
/// "message" property containing the error, and a bad status code.
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct ApiError {
|
|
||||||
message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct LoginResponse {
|
|
||||||
jwt_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn make_request<T>(request: Request) -> Result<T, UiError>
|
|
||||||
where
|
|
||||||
T: for<'a> Deserialize<'a>,
|
|
||||||
{
|
|
||||||
let window = web_sys::window().unwrap();
|
|
||||||
|
|
||||||
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
|
||||||
let resp: Response = resp_value.dyn_into().unwrap();
|
|
||||||
let ok = resp.ok();
|
|
||||||
|
|
||||||
let json = JsFuture::from(resp.json()?).await?;
|
|
||||||
console::log_1(&json);
|
|
||||||
//if ok, attempt to deserialize into T.
|
|
||||||
//if not ok, attempt to deserialize into struct with message, and fall back
|
|
||||||
//if that fails.
|
|
||||||
if ok {
|
|
||||||
let data: T = json.into_serde()?;
|
|
||||||
Ok(data)
|
|
||||||
} else {
|
|
||||||
let data: ApiError = json.into_serde()?;
|
|
||||||
Err(UiError::ApiError(data.message.unwrap_or_else(|| {
|
|
||||||
let status_text = resp.status_text();
|
|
||||||
let status = resp.status();
|
|
||||||
format!("[{}] - {} - unknown api error", status, status_text)
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn login(username: &str, password: &str) -> Result<String, UiError> {
|
|
||||||
let mut opts = RequestInit::new();
|
|
||||||
opts.method("POST");
|
|
||||||
opts.mode(RequestMode::Cors);
|
|
||||||
opts.credentials(RequestCredentials::Include);
|
|
||||||
|
|
||||||
let body = JsValue::from_str(
|
|
||||||
&serde_json::json!({
|
|
||||||
"username": username,
|
|
||||||
"password": password
|
|
||||||
})
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
opts.body(Some(&body));
|
|
||||||
let url = format!("http://localhost:10000/login");
|
|
||||||
|
|
||||||
let request = Request::new_with_str_and_init(&url, &opts)?;
|
|
||||||
request.headers().set("Content-Type", "application/json")?;
|
|
||||||
request.headers().set("Accept", "application/json")?;
|
|
||||||
|
|
||||||
let response: LoginResponse = make_request(request).await?;
|
|
||||||
Ok(response.jwt_token)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh_jwt() -> Result<String, UiError> {
|
|
||||||
let mut opts = RequestInit::new();
|
|
||||||
opts.method("POST");
|
|
||||||
opts.mode(RequestMode::Cors);
|
|
||||||
opts.credentials(RequestCredentials::Include);
|
|
||||||
|
|
||||||
let url = format!("http://localhost:10000/refresh");
|
|
||||||
|
|
||||||
let request = Request::new_with_str_and_init(&url, &opts)?;
|
|
||||||
request.headers().set("Content-Type", "application/json")?;
|
|
||||||
request.headers().set("Accept", "application/json")?;
|
|
||||||
|
|
||||||
let response: LoginResponse = make_request(request).await?;
|
|
||||||
Ok(response.jwt_token)
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
use graphql_client::web::Client;
|
|
||||||
use graphql_client::GraphQLQuery;
|
|
||||||
use graphql_client_web::Response;
|
|
||||||
|
|
||||||
use super::ResponseExt;
|
|
||||||
use crate::error::UiError;
|
|
||||||
|
|
||||||
#[derive(GraphQLQuery)]
|
|
||||||
#[graphql(
|
|
||||||
schema_path = "graphql/schema.graphql",
|
|
||||||
query_path = "graphql/queries/get_user_variable.graphql",
|
|
||||||
response_derives = "Debug"
|
|
||||||
)]
|
|
||||||
struct GetUserVariable;
|
|
||||||
|
|
||||||
#[derive(GraphQLQuery)]
|
|
||||||
#[graphql(
|
|
||||||
schema_path = "graphql/schema.graphql",
|
|
||||||
query_path = "graphql/queries/rooms_for_user.graphql",
|
|
||||||
response_derives = "Debug"
|
|
||||||
)]
|
|
||||||
struct RoomsForUser;
|
|
||||||
|
|
||||||
pub async fn get_user_variable(
|
|
||||||
jwt_token: &str,
|
|
||||||
room_id: &str,
|
|
||||||
user_id: &str,
|
|
||||||
variable_name: &str,
|
|
||||||
) -> Result<i64, UiError> {
|
|
||||||
let mut client = Client::new("http://localhost:10000/graphql");
|
|
||||||
client.add_header("Authorization", &format!("Bearer {}", jwt_token));
|
|
||||||
|
|
||||||
let variables = get_user_variable::Variables {
|
|
||||||
room_id: room_id.to_owned(),
|
|
||||||
user_id: user_id.to_owned(),
|
|
||||||
variable: variable_name.to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = client.call(GetUserVariable, variables).await?;
|
|
||||||
let response: graphql_client_web::Response<get_user_variable::ResponseData> = response;
|
|
||||||
Ok(response.data()?.variable.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn rooms_for_user(
|
|
||||||
jwt_token: &str,
|
|
||||||
user_id: &str,
|
|
||||||
) -> Result<Vec<rooms_for_user::RoomsForUserUserRoomsRooms>, UiError> {
|
|
||||||
let mut client = Client::new("http://localhost:10000/graphql");
|
|
||||||
client.add_header("Authorization", &format!("Bearer {}", jwt_token));
|
|
||||||
|
|
||||||
let variables = rooms_for_user::Variables {
|
|
||||||
user_id: user_id.to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = client.call(RoomsForUser, variables).await?;
|
|
||||||
let response: Response<rooms_for_user::ResponseData> = response;
|
|
||||||
Ok(response.data()?.user_rooms.rooms)
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
use graphql_client_web::Response;
|
|
||||||
|
|
||||||
use crate::error::UiError;
|
|
||||||
|
|
||||||
pub mod auth;
|
|
||||||
pub mod dicebot;
|
|
||||||
|
|
||||||
/// Extensions to the GraphQL web response type to add convenience,
|
|
||||||
/// particularly when working with errors.
|
|
||||||
trait ResponseExt<T> {
|
|
||||||
/// Get the data from the response, or gather all server-side
|
|
||||||
/// errors into a UiError variant.
|
|
||||||
fn data(self) -> Result<T, UiError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> ResponseExt<T> for Response<T> {
|
|
||||||
fn data(self) -> Result<T, UiError> {
|
|
||||||
let data = self.data;
|
|
||||||
let errors = self.errors;
|
|
||||||
|
|
||||||
let data = data.ok_or_else(|| {
|
|
||||||
UiError::ApiError(
|
|
||||||
errors
|
|
||||||
.map(|errors| {
|
|
||||||
errors
|
|
||||||
.into_iter()
|
|
||||||
.map(|e| e.to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(",")
|
|
||||||
})
|
|
||||||
.unwrap_or("unknown error".into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(data)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
use crate::api;
|
|
||||||
use crate::error::UiError;
|
|
||||||
use crate::state::{Action, WebUiDispatcher};
|
|
||||||
use std::rc::Rc;
|
|
||||||
use wasm_bindgen::{prelude::Closure, JsCast};
|
|
||||||
use wasm_bindgen_futures::spawn_local;
|
|
||||||
use web_sys::FocusEvent;
|
|
||||||
use yew::prelude::*;
|
|
||||||
use yewdux::dispatch::Dispatcher;
|
|
||||||
use yewdux::prelude::*;
|
|
||||||
use yewtil::NeqAssign;
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub(crate) struct YewduxErrorMessage {
|
|
||||||
dispatch: WebUiDispatcher,
|
|
||||||
link: ComponentLink<YewduxErrorMessage>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) type ErrorMessage = WithDispatch<YewduxErrorMessage>;
|
|
||||||
|
|
||||||
impl YewduxErrorMessage {
|
|
||||||
fn view_error(&self, error: &str) -> Html {
|
|
||||||
html! {
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
{ error }
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for YewduxErrorMessage {
|
|
||||||
type Message = ();
|
|
||||||
type Properties = WebUiDispatcher;
|
|
||||||
|
|
||||||
fn create(dispatch: Self::Properties, link: ComponentLink<Self>) -> Self {
|
|
||||||
Self { dispatch, link }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, action: Self::Message) -> ShouldRender {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change(&mut self, dispatch: Self::Properties) -> ShouldRender {
|
|
||||||
self.dispatch.neq_assign(dispatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rendered(&mut self, _first_render: bool) {}
|
|
||||||
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
html! {
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
for self.dispatch.state().error_messages.iter().map(|error| {
|
|
||||||
self.view_error(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn destroy(&mut self) {}
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
use crate::api;
|
|
||||||
use crate::error::UiError;
|
|
||||||
use crate::state::{Action, WebUiDispatcher};
|
|
||||||
use std::rc::Rc;
|
|
||||||
use wasm_bindgen_futures::spawn_local;
|
|
||||||
use web_sys::FocusEvent;
|
|
||||||
use yew::prelude::*;
|
|
||||||
use yewdux::dispatch::Dispatcher;
|
|
||||||
use yewdux::prelude::*;
|
|
||||||
use yewtil::NeqAssign;
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub(crate) struct YewduxLogin {
|
|
||||||
dispatch: Rc<WebUiDispatcher>,
|
|
||||||
link: ComponentLink<YewduxLogin>,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum LoginAction {
|
|
||||||
UpdateUsername(String),
|
|
||||||
UpdatePassword(String),
|
|
||||||
Login,
|
|
||||||
Noop,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) type Login = WithDispatch<YewduxLogin>;
|
|
||||||
|
|
||||||
async fn do_login(
|
|
||||||
dispatch: &WebUiDispatcher,
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
) -> Result<(), UiError> {
|
|
||||||
let jwt = api::auth::login(username, password).await?;
|
|
||||||
dispatch.send(Action::Login(jwt));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for YewduxLogin {
|
|
||||||
type Message = LoginAction;
|
|
||||||
type Properties = WebUiDispatcher;
|
|
||||||
|
|
||||||
fn create(dispatch: Self::Properties, link: ComponentLink<Self>) -> Self {
|
|
||||||
Self {
|
|
||||||
dispatch: Rc::new(dispatch),
|
|
||||||
link,
|
|
||||||
username: "".to_string(),
|
|
||||||
password: "".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, action: Self::Message) -> ShouldRender {
|
|
||||||
match action {
|
|
||||||
LoginAction::UpdateUsername(username) => self.username = username,
|
|
||||||
LoginAction::UpdatePassword(password) => self.password = password,
|
|
||||||
LoginAction::Login => {
|
|
||||||
let dispatch = self.dispatch.clone();
|
|
||||||
let username = self.username.clone();
|
|
||||||
let password = self.password.clone();
|
|
||||||
|
|
||||||
spawn_local(async move {
|
|
||||||
do_login(&*dispatch, &username, &password).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change(&mut self, dispatch: Self::Properties) -> ShouldRender {
|
|
||||||
self.dispatch.neq_assign(Rc::new(dispatch))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
let do_the_login = self.link.callback(move |e: FocusEvent| {
|
|
||||||
e.prevent_default();
|
|
||||||
LoginAction::Login
|
|
||||||
});
|
|
||||||
|
|
||||||
let update_username = self
|
|
||||||
.link
|
|
||||||
.callback(|e: InputData| LoginAction::UpdateUsername(e.value));
|
|
||||||
|
|
||||||
let update_password = self
|
|
||||||
.link
|
|
||||||
.callback(|e: InputData| LoginAction::UpdatePassword(e.value));
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<div>
|
|
||||||
<form onsubmit=do_the_login>
|
|
||||||
<label for="username">{"Username:"}</label>
|
|
||||||
<input oninput=update_username id="username" name="username" type="text" placeholder="Username" />
|
|
||||||
<label for="password">{"Password:"}</label>
|
|
||||||
<input oninput=update_password id="password" name="password" type="password" placeholder="Password" />
|
|
||||||
<input type="submit" value="Log In" />
|
|
||||||
</form>
|
|
||||||
//<button onclick=refresh_jwt>{ "Refresh JWT" }</button>
|
|
||||||
<div>
|
|
||||||
{ "Current JWT: " }
|
|
||||||
{ self.dispatch.state().jwt_token.as_ref().unwrap_or(&"[not set]".to_string()) }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn destroy(&mut self) {}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
pub mod error_message;
|
|
||||||
pub mod login;
|
|
|
@ -1,34 +0,0 @@
|
||||||
use graphql_client_web::ClientError;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum UiError {
|
|
||||||
#[error("api client error: {0}")]
|
|
||||||
ApiClientError(#[from] ClientError),
|
|
||||||
|
|
||||||
/// General API error, collecting errors from graphql server.
|
|
||||||
#[error("error: {0}")]
|
|
||||||
ApiError(String),
|
|
||||||
|
|
||||||
#[error("login token invalid or expired")]
|
|
||||||
NotLoggedIn,
|
|
||||||
|
|
||||||
#[error("error: {0}")]
|
|
||||||
JsError(String),
|
|
||||||
|
|
||||||
#[error("(de)serialization error: {0}")]
|
|
||||||
SerdeError(#[from] serde_json::Error),
|
|
||||||
|
|
||||||
#[error("JWT validation error: {0}")]
|
|
||||||
JwtError(#[from] jsonwebtoken::errors::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<wasm_bindgen::JsValue> for UiError {
|
|
||||||
fn from(js_error: wasm_bindgen::JsValue) -> UiError {
|
|
||||||
UiError::JsError(
|
|
||||||
js_error
|
|
||||||
.as_string()
|
|
||||||
.unwrap_or("unknown JS error".to_string()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
// async fn test_grpc_web() {
|
|
||||||
// use grpc_web_client::Client as GrpcWebClient;
|
|
||||||
// use tenebrous_rpc::protos::web_api::web_api_client::WebApiClient as TheCloud;
|
|
||||||
// use tenebrous_rpc::protos::web_api::{RoomsListReply, UserIdRequest};
|
|
||||||
|
|
||||||
// let client = GrpcWebClient::new("http://localhost:10000".to_string());
|
|
||||||
// let mut client = TheCloud::new(client);
|
|
||||||
|
|
||||||
// let request = tonic::Request::new(UserIdRequest {
|
|
||||||
// user_id: "WebTonic".into(),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// let response = client.list_room(request).await.unwrap().into_inner();
|
|
||||||
// println!("Room reply: {:?}", response);
|
|
||||||
// }
|
|
|
@ -1,140 +0,0 @@
|
||||||
use crate::components::error_message::ErrorMessage;
|
|
||||||
use crate::components::login::Login;
|
|
||||||
use error::UiError;
|
|
||||||
use rooms::RoomList;
|
|
||||||
use rooms::YewduxRoomList;
|
|
||||||
use state::{Action, AuthState, WebUiDispatcher};
|
|
||||||
use wasm_bindgen::prelude::*;
|
|
||||||
use wasm_bindgen_futures::spawn_local;
|
|
||||||
use yew::prelude::*;
|
|
||||||
use yew_router::prelude::*;
|
|
||||||
use yew_router::{components::RouterAnchor, prelude::*, switch::Permissive};
|
|
||||||
use yewdux::prelude::*;
|
|
||||||
use yewtil::NeqAssign;
|
|
||||||
|
|
||||||
pub mod api;
|
|
||||||
pub mod components;
|
|
||||||
pub mod error;
|
|
||||||
pub mod grpc;
|
|
||||||
pub mod logic;
|
|
||||||
pub mod rooms;
|
|
||||||
pub mod state;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Switch)]
|
|
||||||
pub enum AppRoute {
|
|
||||||
#[to = "/rooms"]
|
|
||||||
Rooms,
|
|
||||||
#[to = "/rooms/{room_id}"]
|
|
||||||
Room { room_id: String },
|
|
||||||
#[to = "/"]
|
|
||||||
Index,
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppRouter = Router<AppRoute>;
|
|
||||||
type AppAnchor = RouterAnchor<AppRoute>; //For rendering clickable links.
|
|
||||||
|
|
||||||
fn render_route(routes: AppRoute) -> Html {
|
|
||||||
match routes {
|
|
||||||
AppRoute::Rooms => {
|
|
||||||
html! {
|
|
||||||
<RoomList />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppRoute::Room { room_id } => {
|
|
||||||
html! {
|
|
||||||
<div>{"This is the specific room page."}</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppRoute::Index => {
|
|
||||||
html! {
|
|
||||||
<div>
|
|
||||||
<ErrorMessage />
|
|
||||||
<RoomList />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct YewduxApp {
|
|
||||||
link: ComponentLink<YewduxApp>,
|
|
||||||
dispatch: WebUiDispatcher,
|
|
||||||
}
|
|
||||||
|
|
||||||
type App = WithDispatch<YewduxApp>;
|
|
||||||
|
|
||||||
async fn refresh_jwt(dispatch: &WebUiDispatcher) {
|
|
||||||
match api::auth::refresh_jwt().await {
|
|
||||||
Ok(jwt) => {
|
|
||||||
dispatch.send(Action::Login(jwt));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
web_sys::console::log_1(&e.to_string().into());
|
|
||||||
dispatch.send(Action::ChangeAuthState(AuthState::NotLoggedIn));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for YewduxApp {
|
|
||||||
type Message = ();
|
|
||||||
type Properties = WebUiDispatcher;
|
|
||||||
|
|
||||||
fn create(dispatch: Self::Properties, link: ComponentLink<Self>) -> Self {
|
|
||||||
Self { dispatch, link }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, _: Self::Message) -> ShouldRender {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change(&mut self, dispatch: Self::Properties) -> ShouldRender {
|
|
||||||
self.dispatch.neq_assign(dispatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rendered(&mut self, first_render: bool) {
|
|
||||||
if first_render {
|
|
||||||
let auth_state = self.dispatch.state().auth_state;
|
|
||||||
|
|
||||||
if auth_state == AuthState::RefreshNeeded {
|
|
||||||
let dispatch = self.dispatch.clone();
|
|
||||||
spawn_local(async move {
|
|
||||||
refresh_jwt(&dispatch).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
let auth_state = self.dispatch.state().auth_state;
|
|
||||||
|
|
||||||
match auth_state {
|
|
||||||
AuthState::RefreshNeeded => {
|
|
||||||
html! {
|
|
||||||
<div>{"Loading..."}</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AuthState::NotLoggedIn => {
|
|
||||||
html! {
|
|
||||||
<Login />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AuthState::LoggedIn => {
|
|
||||||
html! {
|
|
||||||
<div>
|
|
||||||
<div class="alert alert-primary" role="alert">
|
|
||||||
{"Hello World"}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<AppRouter render=AppRouter::render(render_route) />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(start)]
|
|
||||||
pub fn run_app() {
|
|
||||||
yew::start_app::<App>();
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
use crate::{
|
|
||||||
api,
|
|
||||||
state::{Action, Claims, Room, WebUiDispatcher},
|
|
||||||
};
|
|
||||||
use jsonwebtoken::dangerous_insecure_decode;
|
|
||||||
|
|
||||||
pub(crate) type LogicResult = Result<Vec<Action>, Action>;
|
|
||||||
|
|
||||||
pub(crate) trait LogicResultExt {
|
|
||||||
/// Consumes self into the vec of Actions to apply to state,
|
|
||||||
/// either the list of successful actions, or a list containing
|
|
||||||
/// the error action.
|
|
||||||
fn actions(self) -> Vec<Action>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogicResultExt for LogicResult {
|
|
||||||
fn actions(self) -> Vec<Action> {
|
|
||||||
self.unwrap_or_else(|err_action| vec![err_action])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_to_vec(action: Option<Action>) -> Vec<Action> {
|
|
||||||
action.map(|a| vec![a]).unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn refresh_ensured_jwt() -> Result<(String, Option<Action>), Action> {
|
|
||||||
api::auth::refresh_jwt()
|
|
||||||
.await
|
|
||||||
.map(|new_jwt| (new_jwt.clone(), Some(Action::UpdateJwt(new_jwt))))
|
|
||||||
.map_err(|_| Action::Logout)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ensure_jwt(dispatch: &WebUiDispatcher) -> Result<(String, Option<Action>), Action> {
|
|
||||||
//TODO lots of clones here. can we avoid?
|
|
||||||
use jsonwebtoken::errors::ErrorKind;
|
|
||||||
let token = dispatch.state().jwt_token.as_deref().unwrap_or_default();
|
|
||||||
let validation: Result<Claims, _> = dangerous_insecure_decode(token).map(|data| data.claims);
|
|
||||||
|
|
||||||
//If valid, simply return token. If expired, attempt to refresh.
|
|
||||||
//Otherwise, bubble error.
|
|
||||||
let token_and_action = match validation {
|
|
||||||
Ok(_) => (token.to_owned(), None),
|
|
||||||
Err(e) if matches!(e.kind(), ErrorKind::ExpiredSignature) => refresh_ensured_jwt().await?,
|
|
||||||
Err(_) => return Err(Action::Logout), //TODO carry error inside Logout?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(token_and_action)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn fetch_rooms(dispatch: &WebUiDispatcher) -> LogicResult {
|
|
||||||
let (jwt, jwt_update) = ensure_jwt(dispatch)
|
|
||||||
.await
|
|
||||||
.map(|(token, update)| (token, map_to_vec(update)))?;
|
|
||||||
|
|
||||||
let rooms: Vec<Action> = api::dicebot::rooms_for_user(&jwt, &dispatch.state().username)
|
|
||||||
.await
|
|
||||||
.map_err(|e| Action::ErrorMessage(e))?
|
|
||||||
.into_iter()
|
|
||||||
.map(|room| {
|
|
||||||
Action::AddRoom(Room {
|
|
||||||
room_id: room.room_id,
|
|
||||||
display_name: room.display_name,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(rooms.into_iter().chain(jwt_update).collect())
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
use crate::api;
|
|
||||||
use crate::error::UiError;
|
|
||||||
use crate::logic::{self, LogicResultExt};
|
|
||||||
use crate::state::{Action, DispatchExt, Room, WebUiDispatcher};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use wasm_bindgen_futures::spawn_local;
|
|
||||||
use web_sys::console;
|
|
||||||
use yew::prelude::*;
|
|
||||||
use yewdux::dispatch::Dispatcher;
|
|
||||||
use yewdux::prelude::*;
|
|
||||||
use yewtil::NeqAssign;
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub(crate) struct YewduxRoomList {
|
|
||||||
dispatch: WebUiDispatcher,
|
|
||||||
link: ComponentLink<YewduxRoomList>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) type RoomList = WithDispatch<YewduxRoomList>;
|
|
||||||
|
|
||||||
fn view_room(room: &Room) -> Html {
|
|
||||||
html! {
|
|
||||||
<li>
|
|
||||||
{&room.display_name} {" ("}{&room.room_id}{")"}
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_rooms(dispatch: &WebUiDispatcher) -> Result<(), UiError> {
|
|
||||||
let result = logic::fetch_rooms(dispatch).await;
|
|
||||||
let actions = result.actions();
|
|
||||||
|
|
||||||
for action in actions {
|
|
||||||
dispatch.send(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn do_refresh_jwt(dispatch: &WebUiDispatcher) {
|
|
||||||
let refresh = api::auth::refresh_jwt().await;
|
|
||||||
|
|
||||||
match refresh {
|
|
||||||
Ok(jwt) => dispatch.send(Action::UpdateJwt(jwt)),
|
|
||||||
Err(e) => dispatch.dispatch_error(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for YewduxRoomList {
|
|
||||||
type Message = ();
|
|
||||||
type Properties = WebUiDispatcher;
|
|
||||||
|
|
||||||
fn create(dispatch: Self::Properties, link: ComponentLink<Self>) -> Self {
|
|
||||||
Self { dispatch, link }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change(&mut self, dispatch: Self::Properties) -> ShouldRender {
|
|
||||||
self.dispatch.neq_assign(dispatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rendered(&mut self, first_render: bool) {
|
|
||||||
if first_render {
|
|
||||||
let dispatch = Arc::new(self.dispatch.clone());
|
|
||||||
|
|
||||||
spawn_local(async move {
|
|
||||||
//TODO make macro to report errors in some common way:
|
|
||||||
//handle_errors!(do_things(&*dispatch).await)
|
|
||||||
match load_rooms(&*dispatch).await {
|
|
||||||
Err(e) => console::log_1(&format!("Error: {:?}", e).into()),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&self) -> Html {
|
|
||||||
let dispatch = Arc::new(self.dispatch.clone());
|
|
||||||
let dispatch2 = dispatch.clone();
|
|
||||||
let dispatch3 = dispatch.clone();
|
|
||||||
|
|
||||||
let the_future = self.link.callback(move |_| {
|
|
||||||
let dispatch = dispatch.clone();
|
|
||||||
|
|
||||||
spawn_local(async move {
|
|
||||||
//TODO make macro to report errors in some common way:
|
|
||||||
//handle_errors!(do_things(&*dispatch).await)
|
|
||||||
match load_rooms(&*dispatch).await {
|
|
||||||
Err(e) => console::log_1(&format!("Error: {:?}", e).into()),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let refresh_jwt = self.link.callback(move |_| {
|
|
||||||
let dispatch = dispatch3.clone();
|
|
||||||
spawn_local(async move { do_refresh_jwt(&*dispatch).await });
|
|
||||||
});
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<div>
|
|
||||||
<button onclick=the_future>{ "Fetch Rooms" }</button>
|
|
||||||
<button onclick=refresh_jwt>{ "Refresh JWT" }</button>
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
for self.dispatch.state().rooms.iter().map(|room| {
|
|
||||||
view_room(room)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//New oath form
|
|
||||||
|
|
||||||
//Edit oath
|
|
|
@ -1,123 +0,0 @@
|
||||||
use crate::error::UiError;
|
|
||||||
use jsonwebtoken::dangerous_insecure_decode;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use wasm_bindgen::{prelude::Closure, JsCast};
|
|
||||||
use yewdux::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub(crate) struct Claims {
|
|
||||||
exp: usize,
|
|
||||||
sub: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct Room {
|
|
||||||
pub room_id: String,
|
|
||||||
pub display_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
|
||||||
pub(crate) enum AuthState {
|
|
||||||
NotLoggedIn,
|
|
||||||
LoggedIn,
|
|
||||||
RefreshNeeded,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AuthState {
|
|
||||||
fn default() -> Self {
|
|
||||||
AuthState::RefreshNeeded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
pub(crate) struct WebUiState {
|
|
||||||
pub auth_state: AuthState,
|
|
||||||
pub jwt_token: Option<String>,
|
|
||||||
pub rooms: Vec<Room>,
|
|
||||||
pub error_messages: Vec<String>,
|
|
||||||
pub username: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) enum Action {
|
|
||||||
UpdateJwt(String),
|
|
||||||
AddRoom(Room),
|
|
||||||
ErrorMessage(UiError),
|
|
||||||
ClearErrorMessage,
|
|
||||||
ChangeAuthState(AuthState),
|
|
||||||
Login(String),
|
|
||||||
Logout,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WebUiState {
|
|
||||||
fn login(&mut self, jwt_token: String) {
|
|
||||||
//TODO this will not work because we cannot ignore the key to decode the JWT.
|
|
||||||
let jwt_decoding: Result<Claims, _> =
|
|
||||||
dangerous_insecure_decode(&jwt_token).map(|data| data.claims);
|
|
||||||
|
|
||||||
match jwt_decoding {
|
|
||||||
Ok(claims) => {
|
|
||||||
self.jwt_token = Some(jwt_token);
|
|
||||||
self.auth_state = AuthState::LoggedIn;
|
|
||||||
self.username = claims.sub;
|
|
||||||
}
|
|
||||||
Err(e) => self.error_messages.push(e.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Reducer for WebUiState {
|
|
||||||
type Action = Action;
|
|
||||||
|
|
||||||
fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reduce(&mut self, action: Self::Action) -> bool {
|
|
||||||
match action {
|
|
||||||
Action::UpdateJwt(jwt_token) => self.jwt_token = Some(jwt_token),
|
|
||||||
Action::Login(jwt_token) => self.login(jwt_token),
|
|
||||||
Action::Logout => (),
|
|
||||||
Action::AddRoom(room) => self.rooms.push(room.clone()),
|
|
||||||
Action::ChangeAuthState(auth_state) => self.auth_state = auth_state,
|
|
||||||
Action::ErrorMessage(error) => self.error_messages.push(error.to_string()),
|
|
||||||
Action::ClearErrorMessage => {
|
|
||||||
if self.error_messages.len() > 0 {
|
|
||||||
self.error_messages.remove(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) type WebUiDispatcher = DispatchProps<ReducerStore<WebUiState>>;
|
|
||||||
|
|
||||||
pub(crate) trait DispatchExt {
|
|
||||||
/// Dispatch an error message and then clear it from the state
|
|
||||||
/// after a few seconds.
|
|
||||||
fn dispatch_error(&self, error: UiError);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DispatchExt for WebUiDispatcher {
|
|
||||||
fn dispatch_error(&self, error: UiError) {
|
|
||||||
self.send(Action::ErrorMessage(error));
|
|
||||||
|
|
||||||
// This is a very hacky way to do this. At the very least, we
|
|
||||||
// should not leak memory, and preferably there's a cleaner
|
|
||||||
// way to actually dispatch the clear action.
|
|
||||||
let window = web_sys::window().unwrap();
|
|
||||||
let dispatch = self.clone();
|
|
||||||
let clear_it = Closure::wrap(
|
|
||||||
Box::new(move || dispatch.send(Action::ClearErrorMessage)) as Box<dyn Fn()>
|
|
||||||
);
|
|
||||||
|
|
||||||
window
|
|
||||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
|
||||||
clear_it.as_ref().unchecked_ref(),
|
|
||||||
4000,
|
|
||||||
)
|
|
||||||
.expect("could not add clear error handler.");
|
|
||||||
|
|
||||||
clear_it.forget();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
//The wasm application is compiled as javascript into the /pkg
|
|
||||||
//directory. Webpack then replaces this import with what is actually
|
|
||||||
//needed. To import the web assembly, the import FUNCTION must be
|
|
||||||
//used. The import STATEMENT does not work.
|
|
||||||
import './webui.scss';
|
|
||||||
import('./pkg');
|
|
|
@ -1,24 +0,0 @@
|
||||||
{
|
|
||||||
"devDependencies": {
|
|
||||||
"@wasm-tool/wasm-pack-plugin": "^1.4.0",
|
|
||||||
"clean-webpack-plugin": "^4.0.0-alpha.0",
|
|
||||||
"css-loader": "^5.2.6",
|
|
||||||
"html-webpack-plugin": "^5.3.1",
|
|
||||||
"postcss": "^8.3.0",
|
|
||||||
"postcss-loader": "^5.3.0",
|
|
||||||
"sass": "^1.34.1",
|
|
||||||
"sass-loader": "^12.0.0",
|
|
||||||
"style-loader": "^2.0.0",
|
|
||||||
"webpack": "^5.37.0",
|
|
||||||
"webpack-cli": "^4.7.0"
|
|
||||||
},
|
|
||||||
"name": "runic",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Stuff",
|
|
||||||
"main": "index.js",
|
|
||||||
"author": "projectmoon <projectmoon@agnos.is>",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"bootstrap": "^5.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
const path = require("path");
|
|
||||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
|
||||||
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
|
|
||||||
console.log('path:', path.resolve(__dirname, 'crate'));
|
|
||||||
module.exports = {
|
|
||||||
experiments: {
|
|
||||||
asyncWebAssembly: true
|
|
||||||
},
|
|
||||||
entry: './index.js',
|
|
||||||
mode: "development",
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, './dist'),
|
|
||||||
filename: 'index_bundle.js',
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
new CleanWebpackPlugin(),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
title: 'Tenebrous'
|
|
||||||
}),
|
|
||||||
new WasmPackPlugin({
|
|
||||||
crateDirectory: path.resolve(__dirname, "crate"),
|
|
||||||
outDir: path.resolve(__dirname, "pkg"),
|
|
||||||
args: "--log-level warn",
|
|
||||||
extraArgs: "--no-typescript",
|
|
||||||
})
|
|
||||||
],
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.(scss)$/,
|
|
||||||
use: [{
|
|
||||||
// inject CSS to page
|
|
||||||
loader: 'style-loader'
|
|
||||||
}, {
|
|
||||||
// translates CSS into CommonJS modules
|
|
||||||
loader: 'css-loader'
|
|
||||||
}, {
|
|
||||||
// Run postcss actions
|
|
||||||
loader: 'postcss-loader',
|
|
||||||
options: {
|
|
||||||
// `postcssOptions` is needed for postcss 8.x;
|
|
||||||
// if you use postcss 7.x skip the key
|
|
||||||
postcssOptions: {
|
|
||||||
// postcss plugins, can be exported to postcss.config.js
|
|
||||||
plugins: function () {
|
|
||||||
return [
|
|
||||||
require('autoprefixer')
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
// compiles Sass to CSS
|
|
||||||
loader: 'sass-loader'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,2 +0,0 @@
|
||||||
@import "custom";
|
|
||||||
@import "~bootstrap/scss/bootstrap";
|
|
2490
web-ui/yarn.lock
2490
web-ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue