Compare commits

..

52 Commits

Author SHA1 Message Date
projectmoon 86df3c5d1f Do not process commands coming from ourselves (help text)
continuous-integration/drone/push Build is passing Details
2024-09-26 09:18:42 +02:00
projectmoon 38a7e50c5c Don't forget to update xbps on final stage too
continuous-integration/drone/push Build is passing Details
2024-09-25 23:06:20 +02:00
projectmoon e309fd1fc6 Sync xbps and update it before everything else.
continuous-integration/drone/push Build is failing Details
2024-09-25 22:56:02 +02:00
projectmoon 9262fe2cac move xbps update after sync
continuous-integration/drone/push Build is failing Details
2024-09-25 22:41:54 +02:00
projectmoon 724a781e7c Attempt to correct error in docker image
continuous-integration/drone/push Build is failing Details
2024-09-25 22:30:30 +02:00
projectmoon ef074beb96 Drone: Update to Rust 1.80 builder
continuous-integration/drone/push Build is failing Details
continuous-integration/drone Build is failing Details
2024-09-25 21:56:03 +02:00
projectmoon 81a69f329a Update for Rust 1.80.x
continuous-integration/drone/push Build is failing Details
2024-09-25 20:57:19 +02:00
projectmoon c9e7efa61d update to sqlx 0.6
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-13 21:12:04 +02:00
projectmoon f295f2b7b6 Update to Matrix SDK 0.6 (#98)
continuous-integration/drone/push Build is passing Details
Quite a few changes involved. Mostly variable renames and a few changes to `await`s.

Not ready yet because bot cannot login due to some arcane error of `expected value at line 1 column 1`.

Co-authored-by: projectmoon <projectmoon@noreply.git.agnos.is>
Reviewed-on: #98
2023-04-13 19:04:48 +00:00
projectmoon 090ce9be45 Add help topic for variables
continuous-integration/drone/push Build is passing Details
Fixes #60
2023-04-05 07:59:13 +02:00
projectmoon 2a6dff3e07 Update cargo deps
continuous-integration/drone/push Build is failing Details
2023-04-05 07:58:26 +02:00
projectmoon 952f35d53a Rust 1.68 (#99)
continuous-integration/drone/push Build is failing Details
Update to Rust 1.68

Co-authored-by: projectmoon <projectmoon@noreply.git.agnos.is>
Reviewed-on: #99
2023-04-05 05:57:16 +00:00
projectmoon 552daa4746 Add a game system column to room info (#95)
continuous-integration/drone/push Build is passing Details
Adds a new enum and table in preparation for storing game information about a specific room.

Reviewed-on: #95
2022-02-02 20:56:50 +00:00
projectmoon c514b85510 Change modifier order in Cthulhu
continuous-integration/drone/push Build is passing Details
2021-11-06 21:23:51 +00:00
projectmoon 6eb81f43d5 Change CofD modifiers to come after dice pool 2021-11-06 21:23:51 +00:00
projectmoon 44b1e0f649 Switch to working (but somewhat bigger) Void docker image
continuous-integration/drone/push Build is passing Details
2021-11-06 13:47:23 +00:00
projectmoon a8ccdc9cce Update rust test image version for CI.
continuous-integration/drone/push Build is passing Details
2021-11-05 19:37:59 +00:00
projectmoon 13ce7b3ee6 Readme update (aka force build)
continuous-integration/drone/push Build is failing Details
2021-11-05 17:53:53 +00:00
projectmoon 6f09a11586 Upgrade to matrix SDK 0.4. 2021-11-05 15:34:16 +00:00
projectmoon ee3ec18e06 Refactor keep-drop parsing into function, better error handling. (#93)
continuous-integration/drone/push Build is passing Details
This commit refactors the keep-drop parsing into two separate
functions: one for extracting keep-drop text, and one for actually
doing something with the extracted values. An intermediate enum is
introduced to contain extracted text, instead of relying on Ok/Err
values directly for figuring out what to do with the values.

This allows us to express "this behavior is correct, and all others
are not" instead of using a "fall back to secondary functionality"
approach.

Reviewed-on: #93
Co-Authored-By: projectmoon <projectmoon@noreply.git.agnos.is>
Co-Committed-By: projectmoon <projectmoon@noreply.git.agnos.is>
2021-09-30 21:16:00 +00:00
projectmoon 126548d868 Do not panic on invalid dice/sides amount for keep/drop.
continuous-integration/drone/push Build is passing Details
Insted of unwrap(), map error to a nom parser error. Not the best-est
solution, but it is functional. The TooLarge value seems appropriate.
2021-09-26 14:15:12 +00:00
Matthew Sparks 7e7e9e534e Adding None enum to keep/drop, cleaning up matches
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-09-24 23:03:20 -04:00
Matthew Sparks 2d9853fbf0 Updating README for new drop command
continuous-integration/drone/pr Build is passing Details
2021-09-17 23:15:55 -04:00
Matthew Sparks 3d6210b32d Adding enum for exclusive drop/keep
continuous-integration/drone/pr Build is passing Details
2021-09-17 23:11:13 -04:00
Matthew Sparks 8b5973475f Forgot to fix tests, fixing keep/drop Err case
continuous-integration/drone/pr Build is passing Details
2021-09-17 22:18:23 -04:00
Matthew Sparks 1992ef4e08 Updating roll doc
continuous-integration/drone/pr Build is failing Details
2021-09-17 22:08:51 -04:00
Matthew Sparks f904e3a948 Updating match blocks for keep/drop
continuous-integration/drone/pr Build is failing Details
2021-09-17 21:45:30 -04:00
Matthew Sparks 8317f40f61 Updating README for keep/drop
continuous-integration/drone/pr Build is passing Details
2021-09-16 23:25:26 -04:00
Matthew Sparks 069ee47364 Adding drop function 2021-09-16 22:55:11 -04:00
Matthew Sparks dc242182f4 Fix string comparison in keep/count check, and add test cases 2021-09-07 23:59:49 -04:00
Matthew Sparks 15163ac11d Adding calculations for keep, and adding validation on keep input 2021-09-07 22:10:14 -04:00
Matthew Sparks 1860eaf378 Adding parsing for keeping highest dice 2021-09-06 21:43:46 -04:00
Matthew Sparks 2654887d8c Initial commit to add keep to dice struct and preserve parser test cases 2021-09-06 21:43:46 -04:00
projectmoon 125f3d0cee Fix drone yml to produce docker images again.
continuous-integration/drone/push Build is passing Details
2021-09-06 23:58:05 +00:00
projectmoon a4c3d34a97 Version 0.13.1
continuous-integration/drone/push Build is passing Details
2021-09-06 22:21:24 +00:00
projectmoon 86fbb05e54 Run Drone CI on tags
continuous-integration/drone/push Build is passing Details
2021-09-06 22:18:06 +00:00
projectmoon 661a943672 Readme Updates (#91)
continuous-integration/drone/push Build was killed Details
Add contributing information.

Add support/community section.

Add matrix room badge
Co-Authored-By: projectmoon <projectmoon@noreply.git.agnos.is>
Co-Committed-By: projectmoon <projectmoon@noreply.git.agnos.is>
2021-09-06 22:15:20 +00:00
projectmoon d65715dee6 Remove example room ID from tonic_client
continuous-integration/drone/push Build is passing Details
2021-09-05 20:38:45 +00:00
projectmoon 55a3bfb861 Update readme for crates.io installation. 2021-09-05 20:38:09 +00:00
projectmoon 0050810182 Fix dicebot readme link
continuous-integration/drone/push Build is passing Details
2021-09-05 20:22:42 +00:00
projectmoon 3ba546d4a4 Add metadata to rpc package.
continuous-integration/drone/push Build is passing Details
2021-09-05 20:14:56 +00:00
projectmoon ffded7b572 Add metadata to rpc package. 2021-09-05 20:14:13 +00:00
projectmoon cf93d14913 Version 0.13.0
continuous-integration/drone/push Build is passing Details
2021-09-05 19:08:27 +00:00
projectmoon cf6dd96b34 Update sqlx and refinery to newer versions (#88)
continuous-integration/drone/push Build is passing Details
For some reason, also required rewriting database tests to deal with
tempfile deleting files after scope drop. This never used to occur,
but now it does! So now the unit tests are in a closure where the temp
file is dropped at the end of the test. Really should just use sqlx
migrations, then we can get an in-memory database.

Co-authored-by: projectmoon <projectmoon@agnos.is>
Reviewed-on: #88
Co-Authored-By: projectmoon <projectmoon@noreply.git.agnos.is>
Co-Committed-By: projectmoon <projectmoon@noreply.git.agnos.is>
2021-09-05 07:56:41 +00:00
projectmoon c8c6f4d6f0 Fix dependency specification for rpc crate in dicebot.
continuous-integration/drone/push Build is passing Details
2021-09-04 23:24:52 +00:00
projectmoon 2488429edb Version 0.12.0
continuous-integration/drone/push Build is passing Details
2021-09-04 22:23:36 +00:00
projectmoon f68d5ffcc1 Update to versioned matrix SDK.
continuous-integration/drone/push Build is passing Details
2021-09-04 21:37:49 +00:00
projectmoon 473e899275 Merge branch 'kg333-master'
continuous-integration/drone/push Build is passing Details
Merge PR #43 from github to fix docker build.
2021-09-03 09:33:02 +00:00
projectmoon 1f03837bfe Merge branch 'master' of https://github.com/kg333/matrix-dicebot into kg333-master 2021-09-03 09:32:48 +00:00
projectmoon 0059e3d133 Revert "Initial prototype of web UI and web API."
continuous-integration/drone/push Build is failing Details
This reverts commit cab856241d.
2021-09-03 09:29:52 +00:00
matthew 915b82d0aa Updating GPG key server; sks-keyservers.net is offline permanently 2021-08-28 00:12:12 +00:00
projectmoon cab856241d Initial prototype of web UI and web API.
continuous-integration/drone/push Build is failing Details
This commit shuffles the entire repository around into multiple crates, bringing with it an in-progress web UI and web AI. It was merged prematurely to allow for dependency upgrades of the matrix SDK.

The build should still only produce the dicebot image.
Co-Authored-By: projectmoon <projectmoon@noreply.git.agnos.is>
Co-Committed-By: projectmoon <projectmoon@noreply.git.agnos.is>
2021-07-15 15:04:50 +00:00
84 changed files with 2995 additions and 8964 deletions

View File

@ -3,7 +3,7 @@ name: build-and-test
steps:
- name: test
image: rust:1.51
image: rust:1.80
commands:
- apt-get update
- apt-get install -y cmake
@ -14,8 +14,9 @@ steps:
- name: docker
image: plugins/docker
when:
branch:
- master
ref:
- refs/tags/v*
- refs/heads/master
settings:
auto_tag: true
username:

3
.gitignore vendored
View File

@ -12,6 +12,3 @@ bigboy
.#*
*.sqlite
.tmp*
node_modules
dist/
yarn-error.log

3910
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,5 @@
members = [
"dicebot",
"rpc",
"api",
"web-ui/crate",
"rpc"
]

View File

@ -1,16 +1,15 @@
# 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 -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 rustup-init -qy
# 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
@ -20,7 +19,10 @@ ADD . ./
RUN . /root/.cargo/env && cargo build --release
# 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++
COPY --from=builder \
/root/src/target/release/dicebot \

View File

@ -1,6 +1,7 @@
# 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
to [GitHub][github-repo]._
@ -24,6 +25,23 @@ System.
* Works in encrypted or unencrypted Matrix rooms.
* 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
### Docker Image
@ -46,6 +64,17 @@ root of the repository.
After pulling or building the image, see [instructions on how to use
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
Precompiled executables are not yet available. Clone this repository
@ -89,8 +118,16 @@ expressions.
!r 3d12 - 5d2 + 3 - 7d3 + 20d20
```
This system does not yet have the capability to handle things like D&D
5e advantage or disadvantage.
#### Keep/Drop Dice
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
@ -241,6 +278,7 @@ The most basic plans are:
* Perhaps some sort of character sheet integration. But for that, we
would need a sheet service.
* Use environment variables instead of config file in Docker image.
* Per-system game rules.
## Credits
@ -254,3 +292,9 @@ support added for Chronicles of Darkness and Call of Cthulhu.
[main-repo]: https://git.agnos.is/projectmoon/tenebrous-dicebot
[github-repo]: https://github.com/ProjectMoon/matrix-dicebot
[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

View File

@ -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" }

View File

@ -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"

1277
api/dist/app.bundle.js vendored

File diff suppressed because one or more lines are too long

9
api/dist/index.html vendored
View File

@ -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>

View File

@ -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(())
}

View File

@ -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 }))
}

View File

@ -1,4 +0,0 @@
use tenebrous_api::schema;
fn main() {
println!("{}", schema::schema().as_schema_language());
}

View File

@ -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,
}
}

View File

@ -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()
}
}

View File

@ -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(())
// }

View File

@ -1,5 +0,0 @@
pub mod api;
pub mod auth;
pub mod config;
pub mod errors;
pub mod schema;

View File

@ -1,5 +0,0 @@
#[rocket::main]
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
tenebrous_api::api::run().await?;
Ok(())
}

View File

@ -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(),
)
}

View File

@ -1,11 +1,12 @@
[package]
name = "tenebrous-dicebot"
version = "0.10.0"
authors = ["Taylor C. Richberger <taywee@gmx.com>", "projectmoon <projectmoon@agnos.is>"]
version = "0.13.2"
rust-version = "1.68"
authors = ["projectmoon <projectmoon@agnos.is>", "Taylor C. Richberger <taywee@gmx.com>"]
edition = "2018"
license = 'AGPL-3.0-or-later'
description = 'An async Matrix dice bot for role-playing games'
readme = 'README.md'
readme = '../README.md'
repository = 'https://git.agnos.is/projectmoon/matrix-dicebot'
keywords = ["games", "dice", "matrix", "bot"]
categories = ["games"]
@ -14,6 +15,8 @@ categories = ["games"]
tonic-build = "0.4"
[dependencies]
# indexmap version locked fixes a dependency cycle.
# indexmap = "=1.6.2"
log = "0.4"
tracing-subscriber = "0.2"
toml = "0.5"
@ -30,18 +33,19 @@ combine = "4.5"
futures = "0.3"
html2text = "0.2"
phf = { version = "0.8", features = ["macros"] }
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "master" }
refinery = { version = "0.5", features = ["rusqlite"]}
barrel = { version = "0.6", features = ["sqlite3"] }
matrix-sdk = { version = "0.6" }
refinery = { version = "0.8", features = ["rusqlite"]}
barrel = { version = "0.7", features = ["sqlite3"] }
strum = { version = "0.22", features = ["derive"] }
tempfile = "3"
substring = "1.4"
fuse-rust = "0.2"
tonic = { version = "0.4" }
tonic = "0.4"
prost = "0.7"
tenebrous-rpc = { path = "../rpc" }
tenebrous-rpc = { path = "../rpc", version = "0.1.0" }
[dependencies.sqlx]
version = "0.5"
version = "0.6"
features = [ "offline", "sqlite", "runtime-tokio-native-tls" ]
[dependencies.serde]

View File

@ -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" ]

View File

@ -6,23 +6,52 @@
use std::fmt;
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)]
pub struct Dice {
pub(crate) count: 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 {
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 {
pub fn new(count: u32, sides: u32) -> Dice {
Dice { count, sides }
pub fn new(count: u32, sides: u32, keep_drop: KeepOrDrop) -> Dice {
Dice {
count,
sides,
keep_drop,
}
}
}

View File

@ -4,6 +4,8 @@
* project.
*/
use nom::bytes::complete::take_while;
use nom::error::ErrorKind as NomErrorKind;
use nom::Err as NomErr;
use nom::{
alt, bytes::complete::tag, character::complete::digit1, complete, many0, named,
sequence::tuple, tag, IResult,
@ -31,13 +33,74 @@ enum Sign {
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> {
let (input, (count, _, sides)) = tuple((digit1, tag("d"), digit1))(input)?;
Ok((
input,
Dice::new(count.parse().unwrap(), sides.parse().unwrap()),
))
let count: u32 = count.parse().map_err(|_| too_big!(count))?;
let sides = sides.parse().map_err(|_| too_big!(sides))?;
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
@ -108,16 +171,103 @@ mod tests {
use super::*;
#[test]
fn dice_test() {
assert_eq!(parse_dice("2d4"), Ok(("", Dice::new(2, 4))));
assert_eq!(parse_dice("20d40"), Ok(("", Dice::new(20, 40))));
assert_eq!(parse_dice("8d7"), Ok(("", Dice::new(8, 7))));
assert_eq!(
parse_dice("2d4"),
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]
fn element_test() {
assert_eq!(
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!(
parse_element(" \t\n\r\n 8 \n"),
@ -139,14 +289,21 @@ mod tests {
parse_signed_element(" \t\n\r\n- 8d4 \n"),
Ok((
" \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!(
parse_signed_element(" \t\n\r\n+ 8d4 \n"),
Ok((
" \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((
"",
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!(
parse_element_expression(" - 8d4 \n "),
Ok((
" \n ",
ElementExpression(vec![SignedElement::Negative(Element::Dice(Dice::new(
8, 4
8,
4,
KeepOrDrop::None
)))])
))
);
assert_eq!(
parse_element_expression("\t3d4 + 7 - 5 - 6d12 + 1d1 + 53 1d5 "),
parse_element_expression("\t3d4k2 + 7 - 5 - 6d12dh3 + 1d1 + 53 1d5 "),
Ok((
" 1d5 ",
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::Negative(Element::Bonus(5)),
SignedElement::Negative(Element::Dice(Dice::new(6, 12))),
SignedElement::Positive(Element::Dice(Dice::new(1, 1))),
SignedElement::Negative(Element::Dice(Dice::new(6, 12, KeepOrDrop::Drop(3)))),
SignedElement::Positive(Element::Dice(Dice::new(1, 1, KeepOrDrop::None))),
SignedElement::Positive(Element::Bonus(53)),
])
))

View File

@ -4,6 +4,7 @@
* project.
*/
use crate::basic::dice;
use crate::basic::dice::KeepOrDrop;
use rand::prelude::*;
use std::fmt;
use std::ops::{Deref, DerefMut};
@ -19,15 +20,27 @@ pub trait Rolled {
}
#[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 {
pub fn rolls(&self) -> &[u32] {
&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 {
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 {
write!(f, "{}", self.rolled_value())?;
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() {
write!(f, " ({}", first)?;
if drop != 0 {
write!(f, " ([{}]", first.1)?;
} else {
write!(f, " ({}", first.1)?;
}
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, ")")?;
}
@ -58,11 +81,17 @@ impl Roll for dice::Dice {
fn roll(&self) -> DiceRoll {
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))
.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::*;
#[test]
fn dice_roll_display_test() {
assert_eq!(DiceRoll(vec![1, 3, 4]).to_string(), "8 (1 + 3 + 4)");
assert_eq!(DiceRoll(vec![]).to_string(), "0");
assert_eq!(DiceRoll(vec![1, 3, 4], 3, 0).to_string(), "8 (1 + 3 + 4)");
assert_eq!(DiceRoll(vec![], 0, 0).to_string(), "0");
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)"
);
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]
fn element_roll_display_test() {
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)"
);
assert_eq!(ElementRoll::Bonus(7).to_string(), "7");
@ -218,11 +255,11 @@ mod tests {
#[test]
fn signed_element_roll_display_test() {
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)"
);
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)"
);
assert_eq!(
@ -239,14 +276,14 @@ mod tests {
fn element_expression_roll_display_test() {
assert_eq!(
ElementExpressionRoll(vec![SignedElementRoll::Positive(ElementRoll::Dice(
DiceRoll(vec![1, 3, 4])
DiceRoll(vec![1, 3, 4], 3, 0)
)),])
.to_string(),
"8 (1 + 3 + 4)"
);
assert_eq!(
ElementExpressionRoll(vec![SignedElementRoll::Negative(ElementRoll::Dice(
DiceRoll(vec![1, 3, 4])
DiceRoll(vec![1, 3, 4], 3, 0)
)),])
.to_string(),
"-8 (1 + 3 + 4)"
@ -263,8 +300,8 @@ mod tests {
);
assert_eq!(
ElementExpressionRoll(vec![
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))),
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 2]))),
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))),
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 2], 2, 0))),
SignedElementRoll::Positive(ElementRoll::Bonus(4)),
SignedElementRoll::Negative(ElementRoll::Bonus(7)),
])
@ -273,13 +310,33 @@ mod tests {
);
assert_eq!(
ElementExpressionRoll(vec![
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))),
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 2]))),
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))),
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 2], 2, 0))),
SignedElementRoll::Negative(ElementRoll::Bonus(4)),
SignedElementRoll::Positive(ElementRoll::Bonus(7)),
])
.to_string(),
"-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)"
);
}
}

View File

@ -1,4 +1,4 @@
use matrix_sdk::identifiers::room_id;
use matrix_sdk::ruma::room_id;
use matrix_sdk::Client;
use tenebrous_dicebot::commands;
use tenebrous_dicebot::commands::ResponseExtractor;
@ -29,7 +29,7 @@ async fn main() -> Result<(), BotError> {
let context = Context {
db,
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 {
id: &room_id!("!fakeroomid:example.com"),
display_name: "fake room".to_owned(),

View File

@ -21,7 +21,7 @@ async fn init(config_path: &str) -> Result<(Arc<Config>, Database, Client), BotE
let cfg = Arc::new(cfg);
let sqlite_path = format!("{}/dicebot.sqlite", cfg.database_path());
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))
}
@ -55,7 +55,7 @@ async fn run() -> Result<(), BotError> {
match try_join!(bot, grpc) {
Ok(_) => (),
Err(e) => error!("Error: {}", e),
Err(e) => error!("Error: {:?}", e),
};
Ok(())

View File

@ -1,5 +1,5 @@
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};
async fn create_client(
@ -10,6 +10,7 @@ async fn create_client(
.await?;
let bearer = MetadataValue::from_str(&format!("Bearer {}", shared_secret))?;
let client = DicebotClient::with_interceptor(channel, move |mut req: Request<()>| {
req.metadata_mut().insert("authorization", bearer.clone());
Ok(req)
@ -22,21 +23,11 @@ async fn create_client(
async fn main() -> Result<(), Box<dyn std::error::Error>> {
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 {
user_id: "@projectmoon:agnos.is".into(),
});
let response = client.rooms_for_user(request).await?.into_inner();
// println!("RESPONSE={:?}", response);
// println!("User friendly response is: {:?}", response.value);
println!("Rooms: {:?}", response.rooms);
Ok(())
}

View File

@ -8,12 +8,8 @@ use crate::{
models::Account,
};
use futures::stream::{self, StreamExt};
use matrix_sdk::{
self,
identifiers::{EventId, RoomId},
room::Joined,
Client,
};
use matrix_sdk::ruma::{OwnedEventId, RoomId};
use matrix_sdk::{self, room::Joined, Client};
use std::clone::Clone;
use std::convert::TryFrom;
@ -24,7 +20,7 @@ pub(super) async fn handle_single_result(
cmd_result: &ExecutionResult,
respond_to: &str,
room: &Joined,
event_id: EventId,
event_id: OwnedEventId,
) {
let html = cmd_result.message_html(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
.registered_user()
.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()?
.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)
}

View File

@ -2,32 +2,25 @@ use super::DiceBot;
use crate::db::sqlite::Database;
use crate::db::Rooms;
use crate::error::BotError;
use async_trait::async_trait;
use log::{debug, error, info, warn};
use matrix_sdk::{
self,
events::{
room::member::MemberEventContent,
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
StrippedStateEvent, SyncMessageEvent,
},
room::Room,
EventHandler,
};
use matrix_sdk::ruma::events::room::member::RoomMemberEventContent;
use matrix_sdk::ruma::events::{StrippedStateEvent, SyncMessageLikeEvent};
use matrix_sdk::{self, room::Room, ruma::events::room::message::RoomMessageEventContent};
use matrix_sdk::{Client, DisplayName};
use std::ops::Sub;
use std::time::UNIX_EPOCH;
use std::time::{Duration, SystemTime};
use std::{clone::Clone, time::UNIX_EPOCH};
/// Check if a message is recent enough to actually process. If the
/// message is within "oldest_message_age" seconds, this function
/// returns true. If it's older than that, it returns false and logs a
/// debug message.
fn check_message_age(
event: &SyncMessageEvent<MessageEventContent>,
event: &SyncMessageLikeEvent<RoomMessageEventContent>,
oldest_message_age: u64,
) -> bool {
let sending_time = event
.origin_server_ts
.origin_server_ts()
.to_system_time()
.unwrap_or(UNIX_EPOCH);
@ -54,7 +47,7 @@ fn check_message_age(
/// the bot left and rejoined quickly.
async fn should_process_message<'a>(
bot: &DiceBot,
event: &SyncMessageEvent<MessageEventContent>,
event: &SyncMessageLikeEvent<RoomMessageEventContent>,
) -> Result<(String, String), BotError> {
//Ignore messages that are older than configured duration.
if !check_message_age(event, bot.config.oldest_message_age()) {
@ -68,23 +61,29 @@ async fn should_process_message<'a>(
return Err(BotError::ShouldNotProcessError);
}
let (msg_body, sender_username) = if let SyncMessageEvent {
content:
MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent { body, .. }),
..
},
sender,
..
} = event
{
(
body.clone(),
format!("@{}:{}", sender.localpart(), sender.server_name()),
)
} else {
(String::new(), String::new())
};
let msg_body: String = event
.as_original()
.map(|e| e.content.body())
.map(str::to_string)
.unwrap_or_else(|| String::new());
let sender_username: String = format!(
"@{}:{}",
event.sender().localpart(),
event.sender().server_name()
);
// Do not process messages from the bot itself. Otherwise it might
// try to execute its own commands.
let bot_username = bot
.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))
}
@ -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.
/// Originally adapted from the matrix-rust-sdk examples.
#[async_trait]
impl EventHandler for DiceBot {
async fn on_stripped_state_member(
&self,
pub(super) async fn on_stripped_state_member(
event: StrippedStateEvent<RoomMemberEventContent>,
client: Client,
room: Room,
event: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
let room = match room {
Room::Invited(invited_room) => invited_room,
@ -122,37 +116,48 @@ impl EventHandler for DiceBot {
info!(
"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())
}
}
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 {
Room::Joined(joined_room) => joined_room,
_ => return,
};
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;
}
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)
} else {
return;
};
let results = self
let results = bot
.execute_commands(&room, &sender_username, &msg_body)
.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;
}
}

View File

@ -5,7 +5,12 @@ use crate::db::DbState;
use crate::error::BotError;
use crate::state::DiceBotState;
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::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
/// connected to Matrix until its run() function is called.
#[derive(Clone)]
pub struct DiceBot {
/// A reference to the configuration read in on application start.
config: Arc<Config>,
@ -63,12 +69,14 @@ impl DiceBot {
let device_id: Option<String> = self.db.get_device_id().await?;
let device_id: Option<&str> = device_id.as_deref();
client
.login(username, password, device_id, Some("matrix dice bot"))
.await?;
let no_device_ld_login = || client.login_username(username, password);
let device_id_login = |id| client.login_username(username, password).device_id(id);
let login = device_id.map_or_else(no_device_ld_login, device_id_login);
login.send().await?;
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?;
info!("Recorded new device ID: {}", device_id.as_str());
} else {
@ -79,19 +87,35 @@ impl DiceBot {
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
/// terminated, or a panic occurs. Originally adapted from the
/// matrix-rust-sdk command bot example.
pub async fn run(self) -> Result<(), BotError> {
let client = self.client.clone();
self.login(&client).await?;
self.bind_events().await;
client.set_event_handler(Box::new(self)).await;
info!("Listening for commands");
// TODO replace with sync_with_callback for cleaner shutdown
// process.
client.sync(SyncSettings::default()).await;
client.sync(SyncSettings::default()).await?;
Ok(())
}
@ -121,7 +145,7 @@ impl DiceBot {
&self,
room: &Joined,
sender_username: &str,
event_id: EventId,
event_id: OwnedEventId,
results: Vec<(String, ExecutionResult)>,
) {
if results.len() >= 1 {

View File

@ -332,7 +332,7 @@ mod tests {
macro_rules! dummy_room {
() => {
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(),
secure: false,
}
@ -485,7 +485,7 @@ mod tests {
let ctx = Context {
account: crate::models::Account::default(),
db: db,
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: dummy_room!(),
active_room: dummy_room!(),
username: "username",
@ -527,7 +527,7 @@ mod tests {
let ctx = Context {
account: crate::models::Account::default(),
db: db,
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: dummy_room!(),
active_room: dummy_room!(),
username: "username",
@ -566,7 +566,7 @@ mod tests {
let ctx = Context {
account: crate::models::Account::default(),
db: db.clone(),
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: dummy_room!(),
active_room: dummy_room!(),
username: "username",

View File

@ -45,13 +45,13 @@ pub fn parse_modifiers(input: &str) -> Result<DicePoolModifiers, DiceParsingErro
let (result, rest) = parser.parse(input)?;
if rest.len() == 0 {
convert_to_info(&result)
convert_to_modifiers(&result)
} else {
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::*;
if parsed.len() == 0 {
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> {
//The "modifiers:" part is optional. Assume amounts if no modifier
//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 (amounts, modifiers_str) = parse_amounts(input)?;
let modifiers = parse_modifiers(modifiers_str)?;
let amounts = parse_amounts(&amounts_str)?;
Ok(DicePool::new(amounts, modifiers))
}
@ -175,7 +164,7 @@ mod tests {
#[test]
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_eq!(
result.unwrap(),
@ -186,7 +175,7 @@ mod tests {
#[test]
fn dice_pool_number_with_success_change() {
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_eq!(result.unwrap(), DicePool::easy_with_modifiers(8, modifiers));
}
@ -194,7 +183,7 @@ mod tests {
#[test]
fn dice_pool_with_quality_and_success_change() {
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_eq!(result.unwrap(), DicePool::easy_with_modifiers(8, modifiers));
}
@ -224,20 +213,20 @@ mod tests {
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_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_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_eq!(result.unwrap(), expected);
//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_eq!(result.unwrap(), expected);
}

View File

@ -162,11 +162,12 @@ mod tests {
use super::*;
use management::RegisterCommand;
use url::Url;
use matrix_sdk::ruma::room_id;
macro_rules! dummy_room {
() => {
crate::context::RoomContext {
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
id: &room_id!("!fakeroomid:example.com"),
display_name: "displayname".to_owned(),
secure: false,
}
@ -176,7 +177,7 @@ mod tests {
macro_rules! secure_room {
() => {
crate::context::RoomContext {
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
id: &room_id!("!fakeroomid:example.com"),
display_name: "displayname".to_owned(),
secure: true,
}
@ -195,7 +196,7 @@ mod tests {
let ctx = Context {
account: crate::models::Account::default(),
db: db,
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: secure_room!(),
active_room: secure_room!(),
username: "myusername",
@ -218,7 +219,7 @@ mod tests {
let ctx = Context {
account: crate::models::Account::default(),
db: db,
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: secure_room!(),
active_room: secure_room!(),
username: "myusername",
@ -241,7 +242,7 @@ mod tests {
let ctx = Context {
account: crate::models::Account::default(),
db: db,
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: dummy_room!(),
active_room: dummy_room!(),
username: "myusername",
@ -264,7 +265,7 @@ mod tests {
let ctx = Context {
account: crate::models::Account::default(),
db: db,
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: dummy_room!(),
active_room: dummy_room!(),
username: "myusername",
@ -287,7 +288,7 @@ mod tests {
let ctx = Context {
account: crate::models::Account::default(),
db: db,
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: dummy_room!(),
active_room: dummy_room!(),
username: "myusername",

View File

@ -221,9 +221,9 @@ mod tests {
#[test]
fn pool_whitespace_test() {
parse_command("!pool ns3:8 ").expect("was error");
parse_command(" !pool ns3:8").expect("was error");
parse_command(" !pool ns3:8 ").expect("was error");
parse_command("!pool 8 ns3 ").expect("was error");
parse_command(" !pool 8 ns3").expect("was error");
parse_command(" !pool 8 ns3 ").expect("was error");
}
#[test]

View File

@ -6,7 +6,7 @@ use crate::matrix;
use async_trait::async_trait;
use fuse_rust::{Fuse, FuseProperty, Fuseable};
use futures::stream::{self, StreamExt, TryStreamExt};
use matrix_sdk::{identifiers::UserId, Client};
use matrix_sdk::{ruma::OwnedUserId, Client};
use std::convert::TryFrom;
/// Holds matrix room ID and display name as strings, for use with
@ -62,13 +62,13 @@ async fn get_rooms_for_user(
client: &Client,
user_id: &str,
) -> 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 mut rooms_for_user: Vec<RoomNameAndId> = stream::iter(rooms_for_user)
.filter_map(|room| async move {
Some(room.display_name().await.map(|room_name| RoomNameAndId {
id: room.room_id().to_string(),
name: room_name,
name: room_name.to_string(),
}))
})
.try_collect()

View File

@ -1,8 +1,8 @@
use crate::db::sqlite::Database;
use crate::error::BotError;
use crate::models::Account;
use matrix_sdk::identifiers::{RoomId, UserId};
use matrix_sdk::room::Joined;
use matrix_sdk::ruma::{RoomId, UserId};
use matrix_sdk::Client;
use std::convert::TryFrom;
@ -48,15 +48,16 @@ impl RoomContext<'_> {
// TODO is_direct is a hack; the bot should set eligible rooms
// to Direct Message upon joining, if other contact has
// requested it. Waiting on SDK support.
let display_name = room
let display_name =
room
.display_name()
.await
.ok()
.unwrap_or_default()
.to_string();
.map(|d| d.to_string())
.unwrap_or_default();
let sending_user = UserId::try_from(sending_user)?;
let user_in_room = room.get_member(&sending_user).await.ok().is_some();
let sending_user = <&UserId>::try_from(sending_user)?;
let user_in_room = room.get_member(sending_user).await.ok().is_some();
let is_direct = room.active_members().await?.len() == 2;
Ok(RoomContext {

View File

@ -270,7 +270,7 @@ macro_rules! is_variable {
element: Element::Variable(_),
..
}
);
)
};
}
@ -427,11 +427,12 @@ mod tests {
use crate::db::sqlite::Database;
use crate::parser::dice::{Amount, Element, Operator};
use url::Url;
use matrix_sdk::ruma::room_id;
macro_rules! dummy_room {
() => {
crate::context::RoomContext {
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
id: &room_id!("!fakeroomid:example.com"),
display_name: "displayname".to_owned(),
secure: false,
}
@ -511,7 +512,7 @@ mod tests {
let ctx = Context {
account: crate::models::Account::default(),
db: db,
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: dummy_room!(),
active_room: dummy_room!(),
username: "username",
@ -549,7 +550,7 @@ mod tests {
let ctx = Context {
account: crate::models::Account::default(),
db: db,
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: dummy_room!(),
active_room: dummy_room!(),
username: "username",
@ -587,7 +588,7 @@ mod tests {
let ctx = Context {
account: crate::models::Account::default(),
db: db,
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(),
matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: dummy_room!(),
active_room: dummy_room!(),
username: "username",

View File

@ -4,16 +4,13 @@ use crate::parser::dice::DiceParsingError;
//TOOD convert these to use parse_amounts from the common dice code.
fn parse_modifier(input: &str) -> Result<DiceRollModifier, DiceParsingError> {
if input.ends_with("bb") {
Ok(DiceRollModifier::TwoBonus)
} else if input.ends_with("b") {
Ok(DiceRollModifier::OneBonus)
} else if input.ends_with("pp") {
Ok(DiceRollModifier::TwoPenalty)
} else if input.ends_with("p") {
Ok(DiceRollModifier::OnePenalty)
} else {
Ok(DiceRollModifier::Normal)
match input.trim() {
"bb" => Ok(DiceRollModifier::TwoBonus),
"b" => Ok(DiceRollModifier::OneBonus),
"pp" => Ok(DiceRollModifier::TwoPenalty),
"p" => Ok(DiceRollModifier::OnePenalty),
"" => Ok(DiceRollModifier::Normal),
_ => Err(DiceParsingError::InvalidModifiers),
}
}
@ -21,32 +18,70 @@ fn parse_modifier(input: &str) -> Result<DiceRollModifier, DiceParsingError> {
//Split based on :, send first part to parse_modifier.
//Send second part to parse_amounts
pub fn parse_regular_roll(input: &str) -> Result<DiceRoll, DiceParsingError> {
let input: Vec<&str> = input.trim().split(":").collect();
let (modifiers_str, amounts_str) = match input[..] {
[amounts] => Ok(("", amounts)),
[modifiers, amounts] => Ok((modifiers, amounts)),
_ => Err(DiceParsingError::UnconsumedInput),
}?;
let (amount, modifiers_str) = crate::parser::dice::parse_single_amount(input)?;
let modifier = parse_modifier(modifiers_str)?;
let amount = crate::parser::dice::parse_single_amount(amounts_str)?;
Ok(DiceRoll { modifier, amount })
}
pub fn parse_advancement_roll(input: &str) -> Result<AdvancementRoll, DiceParsingError> {
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 {
existing_skill: amounts,
})
} else {
Err(DiceParsingError::InvalidAmount)
}
}
#[cfg(test)]
mod tests {
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]
fn regular_roll_accepts_single_number() {
@ -72,7 +107,7 @@ mod tests {
#[test]
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_eq!(
DiceRoll {
@ -88,7 +123,7 @@ mod tests {
#[test]
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_eq!(
DiceRoll {
@ -104,7 +139,7 @@ mod tests {
#[test]
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_eq!(
DiceRoll {
@ -120,7 +155,7 @@ mod tests {
#[test]
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_eq!(
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("bb:60 ").is_ok());
assert!(parse_regular_roll(" bb:60").is_ok());
assert!(parse_regular_roll(" bb:60 ").is_ok());
assert!(parse_regular_roll("60bb ").is_ok());
assert!(parse_regular_roll(" 60 bb").is_ok());
assert!(parse_regular_roll(" 60 bb ").is_ok());
assert!(parse_regular_roll("b:60 ").is_ok());
assert!(parse_regular_roll(" b:60").is_ok());
assert!(parse_regular_roll(" b:60 ").is_ok());
assert!(parse_regular_roll("60b ").is_ok());
assert!(parse_regular_roll(" 60 b").is_ok());
assert!(parse_regular_roll(" 60 b ").is_ok());
assert!(parse_regular_roll("pp:60 ").is_ok());
assert!(parse_regular_roll(" pp:60").is_ok());
assert!(parse_regular_roll(" pp:60 ").is_ok());
assert!(parse_regular_roll("60pp ").is_ok());
assert!(parse_regular_roll(" 60 pp").is_ok());
assert!(parse_regular_roll(" 60 pp ").is_ok());
assert!(parse_regular_roll("p:60 ").is_ok());
assert!(parse_regular_roll(" p:60").is_ok());
assert!(parse_regular_roll(" p:60 ").is_ok());
assert!(parse_regular_roll("60p ").is_ok());
assert!(parse_regular_roll(" 60p ").is_ok());
assert!(parse_regular_roll(" 60 p ").is_ok());
}
#[test]

View File

@ -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>()
}

View File

@ -1,2 +1 @@
use refinery::include_migration_mods;
include_migration_mods!("src/db/sqlite/migrator/migrations");

View File

@ -5,7 +5,7 @@ use sqlx::ConnectOptions;
use std::str::FromStr;
use thiserror::Error;
pub mod migrations;
//pub mod migrations;
#[derive(Error, Debug)]
pub enum MigrationError {
@ -16,6 +16,11 @@ pub enum MigrationError {
RefineryError(#[from] refinery::Error),
}
mod embedded {
use refinery::embed_migrations;
embed_migrations!("src/db/sqlite/migrator/migrations");
}
/// Run database migrations against the sqlite database.
pub async fn migrate(db_path: &str) -> Result<(), MigrationError> {
//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);
info!("Running migrations");
migrations::runner().run(&mut conn)?;
embedded::migrations::runner().run(&mut conn)?;
Ok(())
}

View File

@ -53,22 +53,27 @@ impl Rooms for Database {
mod tests {
use crate::db::sqlite::Database;
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();
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
.await
.unwrap();
Database::new(db_path.path().to_str().unwrap())
let db = Database::new(db_path.path().to_str().unwrap())
.await
.unwrap()
.unwrap();
f(db).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn should_process_test() {
let db = create_db().await;
with_db(|db| async move {
let first_check = db
.should_process("myroom", "myeventid")
.await
@ -82,5 +87,7 @@ mod tests {
.expect("should_process failed in first insert");
assert_eq!(second_check, false);
})
.await;
}
}

View File

@ -37,22 +37,27 @@ impl DbState for Database {
mod tests {
use crate::db::sqlite::Database;
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();
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
.await
.unwrap();
Database::new(db_path.path().to_str().unwrap())
let db = Database::new(db_path.path().to_str().unwrap())
.await
.unwrap()
.unwrap();
f(db).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn set_and_get_device_id() {
let db = create_db().await;
with_db(|db| async move {
db.set_device_id("device_id")
.await
.expect("Could not set device ID");
@ -61,19 +66,22 @@ mod tests {
assert!(device_id.is_some());
assert_eq!(device_id.unwrap(), "device_id");
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
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");
assert!(device_id.is_none());
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_update_device_id() {
let db = create_db().await;
with_db(|db| async move {
db.set_device_id("device_id")
.await
.expect("Could not set device ID");
@ -86,5 +94,7 @@ mod tests {
assert!(device_id.is_some());
assert_eq!(device_id.unwrap(), "device_id2");
})
.await;
}
}

View File

@ -91,22 +91,27 @@ mod tests {
use crate::db::sqlite::Database;
use crate::db::Users;
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();
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
.await
.unwrap();
Database::new(db_path.path().to_str().unwrap())
let db = Database::new(db_path.path().to_str().unwrap())
.await
.unwrap()
.unwrap();
f(db).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn create_and_get_full_user_test() {
let db = create_db().await;
with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
username: "myuser".to_string(),
@ -129,12 +134,13 @@ mod tests {
assert_eq!(user.password, Some("abc".to_string()));
assert_eq!(user.account_status, AccountStatus::Registered);
assert_eq!(user.active_room, Some("myroom".to_string()));
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_get_user_with_no_state_record() {
let db = create_db().await;
with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
username: "myuser".to_string(),
@ -164,12 +170,13 @@ mod tests {
//These should be default values because the state record is missing.
assert_eq!(user.active_room, None);
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_insert_without_password() {
let db = create_db().await;
with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
username: "myuser".to_string(),
@ -189,12 +196,13 @@ mod tests {
let user = user.unwrap();
assert_eq!(user.username, "myuser");
assert_eq!(user.password, None);
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_insert_without_active_room() {
let db = create_db().await;
with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
username: "myuser".to_string(),
@ -214,12 +222,13 @@ mod tests {
let user = user.unwrap();
assert_eq!(user.username, "myuser");
assert_eq!(user.active_room, None);
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_update_user() {
let db = create_db().await;
with_db(|db| async move {
let insert_result1 = db
.upsert_user(&User {
username: "myuser".to_string(),
@ -254,12 +263,13 @@ mod tests {
assert_eq!(user.password, Some("123".to_string()));
assert_eq!(user.active_room, Some("room".to_string()));
assert_eq!(user.account_status, AccountStatus::AwaitingActivation);
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_delete_user() {
let db = create_db().await;
with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
username: "myuser".to_string(),
@ -280,27 +290,32 @@ mod tests {
.expect("User retrieval query failed");
assert!(user.is_none());
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn username_not_in_db_returns_none() {
let db = create_db().await;
with_db(|db| async move {
let user = db
.get_user("does not exist")
.await
.expect("Get user query failure");
assert!(user.is_none());
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn authenticate_user_is_some_with_valid_password() {
let db = create_db().await;
with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
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()
})
.await;
@ -315,16 +330,19 @@ mod tests {
assert!(user.is_some());
let user = user.unwrap();
assert_eq!(user.username, "myuser");
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn authenticate_user_is_none_with_wrong_password() {
let db = create_db().await;
with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
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()
})
.await;
@ -337,5 +355,7 @@ mod tests {
.expect("User retrieval query failed");
assert!(user.is_none());
})
.await;
}
}

View File

@ -102,22 +102,27 @@ mod tests {
use super::*;
use crate::db::sqlite::Database;
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();
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
.await
.unwrap();
Database::new(db_path.path().to_str().unwrap())
let db = Database::new(db_path.path().to_str().unwrap())
.await
.unwrap()
.unwrap();
f(db).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
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)
.await
.expect("Could not set variable");
@ -128,12 +133,13 @@ mod tests {
.expect("Could not get variable");
assert_eq!(value, 1);
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
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;
assert!(value.is_err());
@ -141,12 +147,13 @@ mod tests {
value.err().unwrap(),
DataError::KeyDoesNotExist(_)
));
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
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)
.await
.expect("Could not set variable");
@ -160,12 +167,13 @@ mod tests {
value.err().unwrap(),
DataError::KeyDoesNotExist(_)
));
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn count_variables_test() {
let db = create_db().await;
with_db(|db| async move {
for variable_name in &["var1", "var2", "var3"] {
db.set_user_variable("myuser", "myroom", variable_name, 1)
.await
@ -178,12 +186,13 @@ mod tests {
.expect("Could not get count.");
assert_eq!(count, 3);
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn count_variables_respects_user_id() {
let db = create_db().await;
with_db(|db| async move {
for variable_name in &["var1", "var2", "var3"] {
db.set_user_variable("different-user", "myroom", variable_name, 1)
.await
@ -196,12 +205,13 @@ mod tests {
.expect("Could not get count.");
assert_eq!(count, 0);
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn count_variables_respects_room_id() {
let db = create_db().await;
with_db(|db| async move {
for variable_name in &["var1", "var2", "var3"] {
db.set_user_variable("myuser", "different-room", variable_name, 1)
.await
@ -214,12 +224,13 @@ mod tests {
.expect("Could not get count.");
assert_eq!(count, 0);
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn delete_variable_test() {
let db = create_db().await;
with_db(|db| async move {
for variable_name in &["var1", "var2", "var3"] {
db.set_user_variable("myuser", "myroom", variable_name, 1)
.await
@ -240,5 +251,7 @@ mod tests {
let var1 = db.get_user_variable("myuser", "myroom", "var1").await;
assert!(var1.is_err());
assert!(matches!(var1.err().unwrap(), DataError::KeyDoesNotExist(_)));
})
.await;
}
}

View File

@ -18,6 +18,12 @@ pub enum BotError {
#[error("could not retrieve device id")]
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}")]
CommandError(#[from] CommandError),
@ -33,15 +39,15 @@ pub enum BotError {
#[error("could not parse URL")]
UrlParseError(#[from] url::ParseError),
#[error("could not parse ID")]
IdParseError(#[from] matrix_sdk::ruma::IdParseError),
#[error("error in matrix state store: {0}")]
MatrixStateStoreError(#[from] matrix_sdk::StoreError),
#[error("uncategorized matrix SDK error: {0}")]
MatrixError(#[from] matrix_sdk::Error),
#[error("uncategorized matrix SDK base error: {0}")]
MatrixBaseError(#[from] matrix_sdk::BaseError),
#[error("future canceled")]
FutureCanceledError,
@ -79,8 +85,8 @@ pub enum BotError {
#[error("could not convert to proper integer type")]
TryFromIntError(#[from] std::num::TryFromIntError),
#[error("identifier error: {0}")]
IdentifierError(#[from] matrix_sdk::identifiers::Error),
// #[error("identifier error: {0}")]
// IdentifierError(#[from] matrix_sdk::ruma::Error),
#[error("password creation error: {0}")]
PasswordCreationError(argon2::Error),

View File

@ -6,6 +6,9 @@ pub fn parse_help_topic(input: &str) -> Option<HelpTopic> {
"dicepool" => Some(HelpTopic::DicePool),
"dice" => Some(HelpTopic::RollingDice),
"cthulhu" => Some(HelpTopic::Cthulhu),
"variables" => Some(HelpTopic::Variables),
"var" => Some(HelpTopic::Variables),
"variable" => Some(HelpTopic::Variables),
"" => Some(HelpTopic::General),
_ => None,
}
@ -16,6 +19,7 @@ pub enum HelpTopic {
DicePool,
Cthulhu,
RollingDice,
Variables,
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.
"};
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! {"
General Help
@ -117,6 +149,7 @@ impl HelpTopic {
HelpTopic::DicePool => DICEPOOL_HELP,
HelpTopic::Cthulhu => CTHULHU_HELP,
HelpTopic::RollingDice => DICE_HELP,
HelpTopic::Variables => VARIABLES_HELP,
HelpTopic::General => GENERAL_HELP,
}
}

View File

@ -14,3 +14,4 @@ pub mod models;
mod parser;
pub mod rpc;
pub mod state;
pub mod systems;

View File

@ -71,22 +71,27 @@ mod tests {
use super::*;
use crate::db::Users;
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();
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
.await
.unwrap();
Database::new(db_path.path().to_str().unwrap())
let db = Database::new(db_path.path().to_str().unwrap())
.await
.unwrap()
.unwrap();
f(db).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
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")
.await
.expect("Account retrieval didn't work");
@ -95,12 +100,13 @@ mod tests {
let user = account.transient_user().unwrap();
assert_eq!(user.username, "@test:example.com");
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn get_or_create_user_when_user_exists() {
let db = create_db().await;
with_db(|db| async move {
let user = User {
username: "myuser".to_string(),
password: Some("abc".to_string()),
@ -119,5 +125,7 @@ mod tests {
let user_again = account.registered_user().unwrap();
assert_eq!(user, *user_again);
})
.await;
}
}

View File

@ -2,15 +2,12 @@ use std::path::PathBuf;
use futures::stream::{self, StreamExt, TryStreamExt};
use log::error;
use matrix_sdk::{events::room::message::NoticeMessageEventContent, room::Joined, ClientConfig};
use matrix_sdk::{
events::room::message::{InReplyTo, Relation},
events::room::message::{MessageEventContent, MessageType},
events::AnyMessageEventContent,
identifiers::EventId,
Error as MatrixError,
};
use matrix_sdk::{identifiers::RoomId, identifiers::UserId, Client};
use matrix_sdk::ruma::events::room::message::{InReplyTo, RoomMessageEventContent, Relation};
use matrix_sdk::ruma::events::AnyMessageLikeEventContent;
use matrix_sdk::ruma::{RoomId, OwnedEventId, OwnedUserId};
use matrix_sdk::Client;
use matrix_sdk::Error as MatrixError;
use matrix_sdk::room::Joined;
use url::Url;
use crate::{config::Config, error::BotError};
@ -32,12 +29,16 @@ fn extract_error_message(error: MatrixError) -> String {
}
/// 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 client_config = ClientConfig::new().store_path(cache_dir);
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.
@ -59,7 +60,7 @@ pub async fn get_users_in_room(
pub async fn get_rooms_for_user(
client: &Client,
user: &UserId,
user: &OwnedUserId,
) -> Result<Vec<Joined>, MatrixError> {
// Carries errors through, in case we cannot load joined user IDs
// from the room for some reason.
@ -87,7 +88,7 @@ pub async fn send_message(
client: &Client,
room_id: &RoomId,
message: (&str, &str),
reply_to: Option<EventId>,
reply_to: Option<OwnedEventId>,
) {
let (html, plain) = message;
let room = match client.get_joined_room(room_id) {
@ -95,15 +96,13 @@ pub async fn send_message(
_ => return,
};
let mut content = MessageEventContent::new(MessageType::Notice(
NoticeMessageEventContent::html(plain.trim(), html),
));
let mut content = RoomMessageEventContent::notice_html(plain.trim(), html);
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;

View File

@ -151,8 +151,9 @@ where
/// should not have an operator, but every one after that should.
/// Accepts expressions like "8", "10 + variablename", "variablename -
/// 3", etc. This function is currently common to systems that don't
/// deal with XdY rolls. Support for that will be added later.
pub fn parse_amounts(input: &str) -> ParseResult<Vec<Amount>> {
/// deal with XdY rolls. Support for that will be added later. Returns
/// parsed amounts and unconsumed input (e.g. roll modifiers).
pub fn parse_amounts(input: &str) -> ParseResult<(Vec<Amount>, &str)> {
let input = input.trim();
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)
})?;
if rest.len() == 0 {
// Any ParseResult errors will short-circuit the collect.
results.into_iter().collect()
} else {
Err(DiceParsingError::UnconsumedInput)
}
let results: Vec<Amount> = results.into_iter().collect::<ParseResult<_>>()?;
Ok((results, rest))
}
/// Parse an expression that expects a single number or variable. No
/// operators are allowed. This function is common to systems that
/// don't deal with XdY rolls. Currently. this function does not
/// support parsing negative numbers.
pub fn parse_single_amount(input: &str) -> ParseResult<Amount> {
/// support parsing negative numbers. Returns the parsed amount and
/// 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
// should be allowed.
let input = input.trim();
let mut parser = first_amount_parser().map(|amount: ParseResult<Amount>| amount);
let (result, rest) = parser.parse(input)?;
if rest.len() == 0 {
result
} else {
Err(DiceParsingError::UnconsumedInput)
}
Ok((result?, rest))
}
#[cfg(test)]
@ -206,10 +199,13 @@ mod parse_single_amount_tests {
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
(
Amount {
operator: Operator::Plus,
element: Element::Variable("abc".to_string())
}
},
""
)
)
}
@ -233,23 +229,14 @@ mod parse_single_amount_tests {
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
(
Amount {
operator: Operator::Plus,
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_eq!(
result.unwrap(),
(
vec![Amount {
operator: Operator::Plus,
element: Element::Number(1)
}]
}],
""
)
);
let result = parse_amounts("10");
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
(
vec![Amount {
operator: Operator::Plus,
element: Element::Number(10)
}]
}],
""
)
);
}
@ -295,20 +288,26 @@ mod parse_many_amounts_tests {
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
(
vec![Amount {
operator: Operator::Plus,
element: Element::Variable("asdf".to_string())
}]
}],
""
)
);
let result = parse_amounts("nosis");
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
(
vec![Amount {
operator: Operator::Plus,
element: Element::Variable("nosis".to_string())
}]
}],
""
)
);
}

View File

@ -4,7 +4,8 @@ use crate::matrix;
use crate::{config::Config, db::sqlite::Database};
use futures::stream;
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::sync::Arc;
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)
.await?;
Ok(Response::new(GetVariableReply {
user_id: request.user_id.clone(),
room_id: request.room_id.clone(),
variable_name: request.variable_name.clone(),
value,
}))
Ok(Response::new(GetVariableReply { value }))
}
async fn get_all_variables(
@ -81,11 +77,7 @@ impl Dicebot for DicebotRpcService {
.get_user_variables(&request.user_id, &request.room_id)
.await?;
Ok(Response::new(GetAllVariablesReply {
user_id: request.user_id.clone(),
room_id: request.user_id.clone(),
variables,
}))
Ok(Response::new(GetAllVariablesReply { variables }))
}
async fn rooms_for_user(
@ -93,7 +85,7 @@ impl Dicebot for DicebotRpcService {
request: Request<UserIdRequest>,
) -> Result<Response<RoomsListReply>, Status> {
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)
.err_into::<BotError>()
@ -103,7 +95,7 @@ impl Dicebot for DicebotRpcService {
.filter_map(|room: Joined| async move {
let room: Result<Room, _> = room.display_name().await.map(|room_name| Room {
room_id: room.room_id().to_string(),
display_name: room_name,
display_name: room_name.to_string(),
});
Some(room)
@ -120,9 +112,6 @@ impl Dicebot for DicebotRpcService {
rooms.sort_by(sort);
Ok(Response::new(RoomsListReply {
user_id: user_id.into_string(),
rooms,
}))
Ok(Response::new(RoomsListReply { rooms }))
}
}

View File

@ -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 {}

View File

@ -3,18 +3,16 @@ name = "tenebrous-rpc"
version = "0.1.0"
authors = ["projectmoon <projectmoon@agnos.is>"]
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
# 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"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tonic-build = { version = "0.4", default_features = false }
tonic-build = "0.4"
[dependencies]
tonic = { version = "0.4", default_features = false }
tonic = "0.4"
prost = "0.7"

View File

@ -1,16 +1,4 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
if cfg!(feature = "only-client") {
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/"],
)?;
tonic_build::compile_protos("protos/dicebot.proto")?;
Ok(())
}
}

View File

@ -15,10 +15,7 @@ message GetVariableRequest {
}
message GetVariableReply {
string user_id = 1;
string room_id = 2;
string variable_name = 3;
int32 value = 4;
int32 value = 1;
}
message GetAllVariablesRequest {
@ -27,9 +24,7 @@ message GetAllVariablesRequest {
}
message GetAllVariablesReply {
string user_id = 1;
string room_id = 2;
map<string, int32> variables = 3;
map<string, int32> variables = 1;
}
message SetVariableRequest {
@ -53,6 +48,5 @@ message RoomsListReply {
string display_name = 2;
}
string user_id = 1;
repeated Room rooms = 2;
repeated Room rooms = 1;
}

View File

@ -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;
}

View File

@ -1,28 +1,5 @@
pub mod protos {
pub mod web_api {
tonic::include_proto!("web_api");
}
pub mod 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)
}

View File

@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}

View File

691
web-ui/crate/Cargo.lock generated
View File

@ -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",
]

View File

@ -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"] }

View File

@ -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(())
}

View File

@ -1,8 +0,0 @@
query GetUserVariable($roomId: String!, $userId: String!, $variable: String!) {
variable(roomId: $roomId, userId: $userId, variable: $variable) {
roomId
userId
value
variableName
}
}

View File

@ -1,9 +0,0 @@
query RoomsForUser($userId: String!) {
userRooms(userId: $userId) {
userId
rooms {
roomId
displayName
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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) {}
}

View File

@ -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) {}
}

View File

@ -1,2 +0,0 @@
pub mod error_message;
pub mod login;

View File

@ -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()),
)
}
}

View File

@ -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);
// }

View File

@ -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>();
}

View File

@ -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())
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -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');

View File

@ -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"
}
}

View File

@ -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'
}]
}
]
}
};

View File

@ -1,2 +0,0 @@
@import "custom";
@import "~bootstrap/scss/bootstrap";

File diff suppressed because it is too large Load Diff