Compare commits

...

38 Commits

Author SHA1 Message Date
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
41 changed files with 2918 additions and 2069 deletions

View File

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

3145
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,4 +2,5 @@
members = [ members = [
"dicebot", "dicebot",
"rpc"
] ]

View File

@ -1,16 +1,13 @@
# Builder image with development dependencies. # Builder image with development dependencies.
FROM bougyman/voidlinux:glibc as builder FROM ghcr.io/void-linux/void-linux:latest-mini-x86_64 as builder
RUN xbps-install -Syu RUN xbps-install -Syu
RUN xbps-install -Sy base-devel rustup cargo cmake wget gnupg RUN xbps-install -Sy base-devel rustup cmake wget gnupg
RUN xbps-install -Sy openssl-devel libstdc++-devel RUN xbps-install -Sy openssl-devel libstdc++-devel
RUN rustup-init -qy RUN rustup-init -qy
# Install tini for signal processing and zombie killing # Install tini for signal processing and zombie killing
ENV TINI_VERSION v0.19.0 ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/local/bin/tini ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/local/bin/tini
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini.asc /tini.asc
RUN gpg --batch --keyserver hkp://keyserver.ubuntu.com --recv-keys 595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7 \
&& gpg --batch --verify /tini.asc /usr/local/bin/tini
RUN chmod +x /usr/local/bin/tini RUN chmod +x /usr/local/bin/tini
# Build dicebot # Build dicebot
@ -20,7 +17,7 @@ ADD . ./
RUN . /root/.cargo/env && cargo build --release RUN . /root/.cargo/env && cargo build --release
# Final image # Final image
FROM bougyman/voidlinux:tiny FROM ghcr.io/void-linux/void-linux:latest-mini-x86_64
RUN xbps-install -Sy ca-certificates libstdc++ RUN xbps-install -Sy ca-certificates libstdc++
COPY --from=builder \ COPY --from=builder \
/root/src/target/release/dicebot \ /root/src/target/release/dicebot \

View File

@ -1,6 +1,7 @@
# Tenebrous Dicebot # Tenebrous Dicebot
[![Build Status](https://drone.agnos.is/api/badges/projectmoon/tenebrous-dicebot/status.svg)](https://drone.agnos.is/projectmoon/tenebrous-dicebot) [![Build Status](https://drone.agnos.is/api/badges/projectmoon/tenebrous-dicebot/status.svg)](https://drone.agnos.is/projectmoon/tenebrous-dicebot)
[![Matrix Chat](https://img.shields.io/matrix/tenebrous:agnos.is?label=matrix&server_fqdn=matrix.org)][matrix-room]
_This repository is hosted on [Agnos.is Git][main-repo] and mirrored _This repository is hosted on [Agnos.is Git][main-repo] and mirrored
to [GitHub][github-repo]._ to [GitHub][github-repo]._
@ -24,6 +25,23 @@ System.
* Works in encrypted or unencrypted Matrix rooms. * Works in encrypted or unencrypted Matrix rooms.
* Storing variables created by the user. * Storing variables created by the user.
## Support and Community
The project has a Matrix room at [#tenebrous:agnos.is][matrix-room].
It is also possible to make a post in [GitHub
Discussions][github-discussions].
For reporting bugs, we prefer that you open an issue on
[git.agnos.is][agnosis-git-issues]. However, you may also open an
issue on [GitHub][github-issues].
### Development and Contributions
All development occurs on [git.agnos.is][main-repo]. If you wish to
contribute, please open a pull request there. In some cases, pull
requests from GitHub may be accepted. All contributions must be
licensed under [AGPL 3.0 or later][agpl] to be accepted.
## Building and Installation ## Building and Installation
### Docker Image ### Docker Image
@ -46,6 +64,17 @@ root of the repository.
After pulling or building the image, see [instructions on how to use After pulling or building the image, see [instructions on how to use
the Docker image](#running-the-bot). the Docker image](#running-the-bot).
### Install from crates.io
The project can be from [crates.io][crates-io]. To install it, execute
`cargo install tenebrous-dicebot`. This will make the following
executables available on your system:
* `dicebot`: Main dicebot executable.
* `dicebot-cmd`: Run dicebot commands from the command line.
* `dicebot_migrate`: Standalone database migrator (not required).
* `tonic_client`: Test client for the gRPC connection (not required).
### Build from Source ### Build from Source
Precompiled executables are not yet available. Clone this repository Precompiled executables are not yet available. Clone this repository
@ -89,8 +118,16 @@ expressions.
!r 3d12 - 5d2 + 3 - 7d3 + 20d20 !r 3d12 - 5d2 + 3 - 7d3 + 20d20
``` ```
This system does not yet have the capability to handle things like D&D #### Keep/Drop Dice
5e advantage or disadvantage. The bot supports either keeping the highest dice in a roll, or
dropping the highest dice in a roll. This allows the bot to handle
things like D&D 5e advantage or disadvantage.
```
!roll 2d20k1
!r 2d20dh1 + 5
!r 10d10k5 + 10d10dh5 - 2
```
### Storytelling System ### Storytelling System
@ -241,6 +278,7 @@ The most basic plans are:
* Perhaps some sort of character sheet integration. But for that, we * Perhaps some sort of character sheet integration. But for that, we
would need a sheet service. would need a sheet service.
* Use environment variables instead of config file in Docker image. * Use environment variables instead of config file in Docker image.
* Per-system game rules.
## Credits ## Credits
@ -254,3 +292,9 @@ support added for Chronicles of Darkness and Call of Cthulhu.
[main-repo]: https://git.agnos.is/projectmoon/tenebrous-dicebot [main-repo]: https://git.agnos.is/projectmoon/tenebrous-dicebot
[github-repo]: https://github.com/ProjectMoon/matrix-dicebot [github-repo]: https://github.com/ProjectMoon/matrix-dicebot
[roadmap]: https://git.agnos.is/projectmoon/tenebrous-dicebot/wiki/Roadmap [roadmap]: https://git.agnos.is/projectmoon/tenebrous-dicebot/wiki/Roadmap
[crates-io]: https://crates.io/crates/tenebrous-dicebot
[matrix-room]: https://matrix.to/#/#tenebrous:agnos.is
[agnosis-git-issues]: https://git.agnos.is/projectmoon/tenebrous-dicebot/issues
[github-discussions]: https://github.com/ProjectMoon/matrix-dicebot/discussions
[github-issues]: https://github.com/ProjectMoon/matrix-dicebot/issues
[agpl]: https://www.gnu.org/licenses/agpl-3.0.en.html

View File

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

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::fmt;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
//Old stuff, for regular dice rolling. To be moved elsewhere. /// A basic dice roll, in XdY notation, like "1d4" or "3d6".
/// Optionally supports D&D advantage/disadvantge keep-or-drop
/// functionality.
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct Dice { pub struct Dice {
pub(crate) count: u32, pub(crate) count: u32,
pub(crate) sides: u32, pub(crate) sides: u32,
pub(crate) keep_drop: KeepOrDrop,
}
/// Enum indicating how to handle bonuses or penalties using extra
/// dice. If set to Keep, the roll will keep the highest X number of
/// dice in the roll, and add those together. If set to Drop, the
/// opposite is performed, and the lowest X number of dice are added
/// instead. If set to None, then all dice in the roll are added up as
/// normal.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum KeepOrDrop {
/// Keep only the X highest dice for adding up to the total.
Keep(u32),
/// Keep only the X lowest dice (i.e. drop the highest) for adding
/// up to the total.
Drop(u32),
/// Add up all dice in the roll for the total.
None,
} }
impl fmt::Display for Dice { impl fmt::Display for Dice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}d{}", self.count, self.sides) match self.keep_drop {
KeepOrDrop::Keep(keep) => write!(f, "{}d{}k{}", self.count, self.sides, keep),
KeepOrDrop::Drop(drop) => write!(f, "{}d{}dh{}", self.count, self.sides, drop),
KeepOrDrop::None => write!(f, "{}d{}", self.count, self.sides),
}
} }
} }
impl Dice { impl Dice {
pub fn new(count: u32, sides: u32) -> Dice { pub fn new(count: u32, sides: u32, keep_drop: KeepOrDrop) -> Dice {
Dice { count, sides } Dice {
count,
sides,
keep_drop,
}
} }
} }

View File

@ -4,6 +4,8 @@
* project. * project.
*/ */
use nom::bytes::complete::take_while; use nom::bytes::complete::take_while;
use nom::error::ErrorKind as NomErrorKind;
use nom::Err as NomErr;
use nom::{ use nom::{
alt, bytes::complete::tag, character::complete::digit1, complete, many0, named, alt, bytes::complete::tag, character::complete::digit1, complete, many0, named,
sequence::tuple, tag, IResult, sequence::tuple, tag, IResult,
@ -31,13 +33,74 @@ enum Sign {
Minus, Minus,
} }
// Parse a dice expression. Does not eat whitespace /// Intermediate parsed value for a keep-drop expression to indicate
/// which one it is.
enum ParsedKeepOrDrop<'a> {
Keep(&'a str),
Drop(&'a str),
NotPresent,
}
macro_rules! too_big {
($input: expr) => {
NomErr::Error(($input, NomErrorKind::TooLarge))
};
}
/// Parse a dice expression. Does not eat whitespace
fn parse_dice(input: &str) -> IResult<&str, Dice> { fn parse_dice(input: &str) -> IResult<&str, Dice> {
let (input, (count, _, sides)) = tuple((digit1, tag("d"), digit1))(input)?; let (input, (count, _, sides)) = tuple((digit1, tag("d"), digit1))(input)?;
Ok(( let count: u32 = count.parse().map_err(|_| too_big!(count))?;
input, let sides = sides.parse().map_err(|_| too_big!(sides))?;
Dice::new(count.parse().unwrap(), sides.parse().unwrap()), let (input, keep_drop) = parse_keep_or_drop(input, count)?;
)) Ok((input, Dice::new(count, sides, keep_drop)))
}
/// Extract keep/drop number as a string. Fails if the value is not a
/// string.
fn parse_keep_or_drop_text<'a>(
symbol: &'a str,
input: &'a str,
) -> IResult<&'a str, ParsedKeepOrDrop<'a>> {
let (parsed_kd, input) = match tuple::<&str, _, (_, _), _>((tag(symbol), digit1))(input) {
// if ok, one of the expressions is present
Ok((rest, (_, kd_expr))) => match symbol {
"k" => (ParsedKeepOrDrop::Keep(kd_expr), rest),
"dh" => (ParsedKeepOrDrop::Drop(kd_expr), rest),
_ => panic!("Unrecogized keep-drop symbol: {}", symbol),
},
// otherwise absent (attempt to keep all dice)
Err(_) => (ParsedKeepOrDrop::NotPresent, input),
};
Ok((input, parsed_kd))
}
/// Parse keep/drop expression, which consits of "k" or "dh" following
/// a dice expression. For example, "1d4h3" or "1d4dh2".
fn parse_keep_or_drop<'a>(input: &'a str, count: u32) -> IResult<&'a str, KeepOrDrop> {
let (input, keep) = parse_keep_or_drop_text("k", input)?;
let (input, drop) = parse_keep_or_drop_text("dh", input)?;
use ParsedKeepOrDrop::*;
let keep_drop: KeepOrDrop = match (keep, drop) {
//Potential valid Keep expression.
(Keep(keep), NotPresent) => match keep.parse().map_err(|_| too_big!(input))? {
_i if _i > count || _i == 0 => Ok(KeepOrDrop::None),
i => Ok(KeepOrDrop::Keep(i)),
},
//Potential valid Drop expression.
(NotPresent, Drop(drop)) => match drop.parse().map_err(|_| too_big!(input))? {
_i if _i >= count => Ok(KeepOrDrop::None),
i => Ok(KeepOrDrop::Drop(i)),
},
//No Keep or Drop specified; regular behavior.
(NotPresent, NotPresent) => Ok(KeepOrDrop::None),
//Anything else is an error.
_ => Err(NomErr::Error((input, NomErrorKind::Many1))),
}?;
Ok((input, keep_drop))
} }
// Parse a single digit expression. Does not eat whitespace // Parse a single digit expression. Does not eat whitespace
@ -108,16 +171,103 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn dice_test() { fn dice_test() {
assert_eq!(parse_dice("2d4"), Ok(("", Dice::new(2, 4)))); assert_eq!(
assert_eq!(parse_dice("20d40"), Ok(("", Dice::new(20, 40)))); parse_dice("2d4"),
assert_eq!(parse_dice("8d7"), Ok(("", Dice::new(8, 7)))); Ok(("", Dice::new(2, 4, KeepOrDrop::None)))
);
assert_eq!(
parse_dice("20d40"),
Ok(("", Dice::new(20, 40, KeepOrDrop::None)))
);
assert_eq!(
parse_dice("8d7"),
Ok(("", Dice::new(8, 7, KeepOrDrop::None)))
);
assert_eq!(
parse_dice("2d20k1"),
Ok(("", Dice::new(2, 20, KeepOrDrop::Keep(1))))
);
assert_eq!(
parse_dice("100d10k90"),
Ok(("", Dice::new(100, 10, KeepOrDrop::Keep(90))))
);
assert_eq!(
parse_dice("11d10k10"),
Ok(("", Dice::new(11, 10, KeepOrDrop::Keep(10))))
);
assert_eq!(
parse_dice("12d10k11"),
Ok(("", Dice::new(12, 10, KeepOrDrop::Keep(11))))
);
assert_eq!(
parse_dice("12d10k13"),
Ok(("", Dice::new(12, 10, KeepOrDrop::None)))
);
assert_eq!(
parse_dice("12d10k0"),
Ok(("", Dice::new(12, 10, KeepOrDrop::None)))
);
assert_eq!(
parse_dice("20d40dh5"),
Ok(("", Dice::new(20, 40, KeepOrDrop::Drop(5))))
);
assert_eq!(
parse_dice("8d7dh9"),
Ok(("", Dice::new(8, 7, KeepOrDrop::None)))
);
assert_eq!(
parse_dice("8d7dh8"),
Ok(("", Dice::new(8, 7, KeepOrDrop::None)))
);
}
#[test]
fn cant_have_both_keep_and_drop_test() {
let res = parse_dice("1d4k3dh2");
assert!(res.is_err());
match res {
Err(NomErr::Error((_, kind))) => {
assert_eq!(kind, NomErrorKind::Many1);
}
_ => panic!("Got success, expected error"),
}
}
#[test]
fn big_number_of_dice_doesnt_crash_test() {
let res = parse_dice("64378631476346123874527551481376547657868536d4");
assert!(res.is_err());
match res {
Err(NomErr::Error((input, kind))) => {
assert_eq!(kind, NomErrorKind::TooLarge);
assert_eq!(input, "64378631476346123874527551481376547657868536");
}
_ => panic!("Got success, expected error"),
}
}
#[test]
fn big_number_of_sides_doesnt_crash_test() {
let res = parse_dice("1d423562312587425472658956278456298376234876");
assert!(res.is_err());
match res {
Err(NomErr::Error((input, kind))) => {
assert_eq!(kind, NomErrorKind::TooLarge);
assert_eq!(input, "423562312587425472658956278456298376234876");
}
_ => panic!("Got success, expected error"),
}
} }
#[test] #[test]
fn element_test() { fn element_test() {
assert_eq!( assert_eq!(
parse_element(" \t\n\r\n 8d7 \n"), parse_element(" \t\n\r\n 8d7 \n"),
Ok((" \n", Element::Dice(Dice::new(8, 7)))) Ok((" \n", Element::Dice(Dice::new(8, 7, KeepOrDrop::None))))
);
assert_eq!(
parse_element(" \t\n\r\n 3d20k2 \n"),
Ok((" \n", Element::Dice(Dice::new(3, 20, KeepOrDrop::Keep(2)))))
); );
assert_eq!( assert_eq!(
parse_element(" \t\n\r\n 8 \n"), parse_element(" \t\n\r\n 8 \n"),
@ -139,14 +289,21 @@ mod tests {
parse_signed_element(" \t\n\r\n- 8d4 \n"), parse_signed_element(" \t\n\r\n- 8d4 \n"),
Ok(( Ok((
" \n", " \n",
SignedElement::Negative(Element::Dice(Dice::new(8, 4))) SignedElement::Negative(Element::Dice(Dice::new(8, 4, KeepOrDrop::None)))
))
);
assert_eq!(
parse_signed_element(" \t\n\r\n- 8d4k4 \n"),
Ok((
" \n",
SignedElement::Negative(Element::Dice(Dice::new(8, 4, KeepOrDrop::Keep(4))))
)) ))
); );
assert_eq!( assert_eq!(
parse_signed_element(" \t\n\r\n+ 8d4 \n"), parse_signed_element(" \t\n\r\n+ 8d4 \n"),
Ok(( Ok((
" \n", " \n",
SignedElement::Positive(Element::Dice(Dice::new(8, 4))) SignedElement::Positive(Element::Dice(Dice::new(8, 4, KeepOrDrop::None)))
)) ))
); );
} }
@ -158,29 +315,43 @@ mod tests {
Ok(( Ok((
"", "",
ElementExpression(vec![SignedElement::Positive(Element::Dice(Dice::new( ElementExpression(vec![SignedElement::Positive(Element::Dice(Dice::new(
8, 4 8,
4,
KeepOrDrop::None
)))]) )))])
)) ))
); );
assert_eq!(
parse_element_expression("\t2d20k1 + 5"),
Ok((
"",
ElementExpression(vec![
SignedElement::Positive(Element::Dice(Dice::new(2, 20, KeepOrDrop::Keep(1)))),
SignedElement::Positive(Element::Bonus(5)),
])
))
);
assert_eq!( assert_eq!(
parse_element_expression(" - 8d4 \n "), parse_element_expression(" - 8d4 \n "),
Ok(( Ok((
" \n ", " \n ",
ElementExpression(vec![SignedElement::Negative(Element::Dice(Dice::new( ElementExpression(vec![SignedElement::Negative(Element::Dice(Dice::new(
8, 4 8,
4,
KeepOrDrop::None
)))]) )))])
)) ))
); );
assert_eq!( assert_eq!(
parse_element_expression("\t3d4 + 7 - 5 - 6d12 + 1d1 + 53 1d5 "), parse_element_expression("\t3d4k2 + 7 - 5 - 6d12dh3 + 1d1 + 53 1d5 "),
Ok(( Ok((
" 1d5 ", " 1d5 ",
ElementExpression(vec![ ElementExpression(vec![
SignedElement::Positive(Element::Dice(Dice::new(3, 4))), SignedElement::Positive(Element::Dice(Dice::new(3, 4, KeepOrDrop::Keep(2)))),
SignedElement::Positive(Element::Bonus(7)), SignedElement::Positive(Element::Bonus(7)),
SignedElement::Negative(Element::Bonus(5)), SignedElement::Negative(Element::Bonus(5)),
SignedElement::Negative(Element::Dice(Dice::new(6, 12))), SignedElement::Negative(Element::Dice(Dice::new(6, 12, KeepOrDrop::Drop(3)))),
SignedElement::Positive(Element::Dice(Dice::new(1, 1))), SignedElement::Positive(Element::Dice(Dice::new(1, 1, KeepOrDrop::None))),
SignedElement::Positive(Element::Bonus(53)), SignedElement::Positive(Element::Bonus(53)),
]) ])
)) ))

View File

@ -4,6 +4,7 @@
* project. * project.
*/ */
use crate::basic::dice; use crate::basic::dice;
use crate::basic::dice::KeepOrDrop;
use rand::prelude::*; use rand::prelude::*;
use std::fmt; use std::fmt;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
@ -19,15 +20,27 @@ pub trait Rolled {
} }
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct DiceRoll(pub Vec<u32>); /// array of rolls in order, how many dice to keep, and how many to drop
/// keep indicates how many of the highest dice to keep
/// drop indicates how many of the highest dice to drop
pub struct DiceRoll (pub Vec<u32>, usize, usize);
impl DiceRoll { impl DiceRoll {
pub fn rolls(&self) -> &[u32] { pub fn rolls(&self) -> &[u32] {
&self.0 &self.0
} }
pub fn keep(&self) -> usize {
self.1
}
pub fn drop(&self) -> usize {
self.2
}
// only count kept dice in total
pub fn total(&self) -> u32 { pub fn total(&self) -> u32 {
self.0.iter().sum() self.0[self.2..self.1].iter().sum()
} }
} }
@ -41,11 +54,21 @@ impl fmt::Display for DiceRoll {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.rolled_value())?; write!(f, "{}", self.rolled_value())?;
let rolls = self.rolls(); let rolls = self.rolls();
let mut iter = rolls.iter(); let keep = self.keep();
let drop = self.drop();
let mut iter = rolls.iter().enumerate();
if let Some(first) = iter.next() { if let Some(first) = iter.next() {
write!(f, " ({}", first)?; if drop != 0 {
write!(f, " ([{}]", first.1)?;
} else {
write!(f, " ({}", first.1)?;
}
for roll in iter { for roll in iter {
write!(f, " + {}", roll)?; if roll.0 >= keep || roll.0 < drop {
write!(f, " + [{}]", roll.1)?;
} else {
write!(f, " + {}", roll.1)?;
}
} }
write!(f, ")")?; write!(f, ")")?;
} }
@ -58,11 +81,17 @@ impl Roll for dice::Dice {
fn roll(&self) -> DiceRoll { fn roll(&self) -> DiceRoll {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let rolls: Vec<_> = (0..self.count) let mut rolls: Vec<_> = (0..self.count)
.map(|_| rng.gen_range(1..=self.sides)) .map(|_| rng.gen_range(1..=self.sides))
.collect(); .collect();
// sort rolls in descending order
rolls.sort_by(|a, b| b.cmp(a));
DiceRoll(rolls) match self.keep_drop {
KeepOrDrop::Keep(k) => DiceRoll(rolls,k as usize, 0),
KeepOrDrop::Drop(dh) => DiceRoll(rolls,self.count as usize, dh as usize),
KeepOrDrop::None => DiceRoll(rolls,self.count as usize, 0),
}
} }
} }
@ -198,18 +227,26 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn dice_roll_display_test() { fn dice_roll_display_test() {
assert_eq!(DiceRoll(vec![1, 3, 4]).to_string(), "8 (1 + 3 + 4)"); assert_eq!(DiceRoll(vec![1, 3, 4], 3, 0).to_string(), "8 (1 + 3 + 4)");
assert_eq!(DiceRoll(vec![]).to_string(), "0"); assert_eq!(DiceRoll(vec![], 0, 0).to_string(), "0");
assert_eq!( assert_eq!(
DiceRoll(vec![4, 7, 2, 10]).to_string(), DiceRoll(vec![4, 7, 2, 10], 4, 0).to_string(),
"23 (4 + 7 + 2 + 10)" "23 (4 + 7 + 2 + 10)"
); );
assert_eq!(
DiceRoll(vec![20, 13, 11, 10], 3, 0).to_string(),
"44 (20 + 13 + 11 + [10])"
);
assert_eq!(
DiceRoll(vec![20, 13, 11, 10], 4, 1).to_string(),
"34 ([20] + 13 + 11 + 10)"
);
} }
#[test] #[test]
fn element_roll_display_test() { fn element_roll_display_test() {
assert_eq!( assert_eq!(
ElementRoll::Dice(DiceRoll(vec![1, 3, 4])).to_string(), ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0)).to_string(),
"8 (1 + 3 + 4)" "8 (1 + 3 + 4)"
); );
assert_eq!(ElementRoll::Bonus(7).to_string(), "7"); assert_eq!(ElementRoll::Bonus(7).to_string(), "7");
@ -218,11 +255,11 @@ mod tests {
#[test] #[test]
fn signed_element_roll_display_test() { fn signed_element_roll_display_test() {
assert_eq!( assert_eq!(
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))).to_string(), SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))).to_string(),
"8 (1 + 3 + 4)" "8 (1 + 3 + 4)"
); );
assert_eq!( assert_eq!(
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))).to_string(), SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))).to_string(),
"-8 (1 + 3 + 4)" "-8 (1 + 3 + 4)"
); );
assert_eq!( assert_eq!(
@ -239,14 +276,14 @@ mod tests {
fn element_expression_roll_display_test() { fn element_expression_roll_display_test() {
assert_eq!( assert_eq!(
ElementExpressionRoll(vec![SignedElementRoll::Positive(ElementRoll::Dice( ElementExpressionRoll(vec![SignedElementRoll::Positive(ElementRoll::Dice(
DiceRoll(vec![1, 3, 4]) DiceRoll(vec![1, 3, 4], 3, 0)
)),]) )),])
.to_string(), .to_string(),
"8 (1 + 3 + 4)" "8 (1 + 3 + 4)"
); );
assert_eq!( assert_eq!(
ElementExpressionRoll(vec![SignedElementRoll::Negative(ElementRoll::Dice( ElementExpressionRoll(vec![SignedElementRoll::Negative(ElementRoll::Dice(
DiceRoll(vec![1, 3, 4]) DiceRoll(vec![1, 3, 4], 3, 0)
)),]) )),])
.to_string(), .to_string(),
"-8 (1 + 3 + 4)" "-8 (1 + 3 + 4)"
@ -263,8 +300,8 @@ mod tests {
); );
assert_eq!( assert_eq!(
ElementExpressionRoll(vec![ ElementExpressionRoll(vec![
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))), SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))),
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 2]))), SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 2], 2, 0))),
SignedElementRoll::Positive(ElementRoll::Bonus(4)), SignedElementRoll::Positive(ElementRoll::Bonus(4)),
SignedElementRoll::Negative(ElementRoll::Bonus(7)), SignedElementRoll::Negative(ElementRoll::Bonus(7)),
]) ])
@ -273,13 +310,33 @@ mod tests {
); );
assert_eq!( assert_eq!(
ElementExpressionRoll(vec![ ElementExpressionRoll(vec![
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4]))), SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![1, 3, 4], 3, 0))),
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 2]))), SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![1, 2], 2, 0))),
SignedElementRoll::Negative(ElementRoll::Bonus(4)), SignedElementRoll::Negative(ElementRoll::Bonus(4)),
SignedElementRoll::Positive(ElementRoll::Bonus(7)), SignedElementRoll::Positive(ElementRoll::Bonus(7)),
]) ])
.to_string(), .to_string(),
"-2 (-8 (1 + 3 + 4) + 3 (1 + 2) - 4 + 7)" "-2 (-8 (1 + 3 + 4) + 3 (1 + 2) - 4 + 7)"
); );
assert_eq!(
ElementExpressionRoll(vec![
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![4, 3, 1], 3, 0))),
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![12, 2], 1, 0))),
SignedElementRoll::Negative(ElementRoll::Bonus(4)),
SignedElementRoll::Positive(ElementRoll::Bonus(7)),
])
.to_string(),
"7 (-8 (4 + 3 + 1) + 12 (12 + [2]) - 4 + 7)"
);
assert_eq!(
ElementExpressionRoll(vec![
SignedElementRoll::Negative(ElementRoll::Dice(DiceRoll(vec![4, 3, 1], 3, 1))),
SignedElementRoll::Positive(ElementRoll::Dice(DiceRoll(vec![12, 2], 2, 0))),
SignedElementRoll::Negative(ElementRoll::Bonus(4)),
SignedElementRoll::Positive(ElementRoll::Bonus(7)),
])
.to_string(),
"13 (-4 ([4] + 3 + 1) + 14 (12 + 2) - 4 + 7)"
);
} }
} }

View File

@ -1,4 +1,4 @@
use matrix_sdk::identifiers::room_id; use matrix_sdk::ruma::room_id;
use matrix_sdk::Client; use matrix_sdk::Client;
use tenebrous_dicebot::commands; use tenebrous_dicebot::commands;
use tenebrous_dicebot::commands::ResponseExtractor; use tenebrous_dicebot::commands::ResponseExtractor;
@ -29,7 +29,7 @@ async fn main() -> Result<(), BotError> {
let context = Context { let context = Context {
db, db,
account: Account::default(), account: Account::default(),
matrix_client: Client::new(homeserver).expect("Could not create matrix client"), matrix_client: Client::new(homeserver).await.expect("Could not create matrix client"),
origin_room: RoomContext { origin_room: RoomContext {
id: &room_id!("!fakeroomid:example.com"), id: &room_id!("!fakeroomid:example.com"),
display_name: "fake room".to_owned(), display_name: "fake room".to_owned(),

View File

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

View File

@ -23,21 +23,11 @@ async fn create_client(
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = create_client("example-key").await?; let mut client = create_client("example-key").await?;
// let request = tonic::Request::new(GetVariableRequest {
// user_id: "@projectmoon:agnos.is".into(),
// room_id: "!agICWvldGfuCywUVUM:agnos.is".into(),
// variable_name: "stuff".into(),
// });
// let response = client.get_variable(request).await?.into_inner();
let request = tonic::Request::new(UserIdRequest { let request = tonic::Request::new(UserIdRequest {
user_id: "@projectmoon:agnos.is".into(), user_id: "@projectmoon:agnos.is".into(),
}); });
let response = client.rooms_for_user(request).await?.into_inner(); let response = client.rooms_for_user(request).await?.into_inner();
// println!("RESPONSE={:?}", response);
// println!("User friendly response is: {:?}", response.value);
println!("Rooms: {:?}", response.rooms); println!("Rooms: {:?}", response.rooms);
Ok(()) Ok(())
} }

View File

@ -8,12 +8,8 @@ use crate::{
models::Account, models::Account,
}; };
use futures::stream::{self, StreamExt}; use futures::stream::{self, StreamExt};
use matrix_sdk::{ use matrix_sdk::ruma::{OwnedEventId, RoomId};
self, use matrix_sdk::{self, room::Joined, Client};
identifiers::{EventId, RoomId},
room::Joined,
Client,
};
use std::clone::Clone; use std::clone::Clone;
use std::convert::TryFrom; use std::convert::TryFrom;
@ -24,7 +20,7 @@ pub(super) async fn handle_single_result(
cmd_result: &ExecutionResult, cmd_result: &ExecutionResult,
respond_to: &str, respond_to: &str,
room: &Joined, room: &Joined,
event_id: EventId, event_id: OwnedEventId,
) { ) {
let html = cmd_result.message_html(respond_to); let html = cmd_result.message_html(respond_to);
let plain = cmd_result.message_plain(respond_to); let plain = cmd_result.message_plain(respond_to);
@ -112,9 +108,9 @@ fn get_account_active_room(client: &Client, account: &Account) -> Result<Option<
let active_room = account let active_room = account
.registered_user() .registered_user()
.and_then(|u| u.active_room.as_deref()) .and_then(|u| u.active_room.as_deref())
.map(|room_id| RoomId::try_from(room_id)) .map(|room_id| <&RoomId>::try_from(room_id))
.transpose()? .transpose()?
.and_then(|active_room_id| client.get_joined_room(&active_room_id)); .and_then(|active_room_id| client.get_joined_room(active_room_id));
Ok(active_room) Ok(active_room)
} }

View File

@ -2,32 +2,25 @@ use super::DiceBot;
use crate::db::sqlite::Database; use crate::db::sqlite::Database;
use crate::db::Rooms; use crate::db::Rooms;
use crate::error::BotError; use crate::error::BotError;
use async_trait::async_trait;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use matrix_sdk::{ use matrix_sdk::ruma::events::room::member::RoomMemberEventContent;
self, use matrix_sdk::ruma::events::{StrippedStateEvent, SyncMessageLikeEvent};
events::{ use matrix_sdk::{Client, DisplayName};
room::member::MemberEventContent, use matrix_sdk::{self, room::Room, ruma::events::room::message::RoomMessageEventContent};
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
StrippedStateEvent, SyncMessageEvent,
},
room::Room,
EventHandler,
};
use std::ops::Sub; use std::ops::Sub;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use std::{clone::Clone, time::UNIX_EPOCH}; use std::time::UNIX_EPOCH;
/// Check if a message is recent enough to actually process. If the /// Check if a message is recent enough to actually process. If the
/// message is within "oldest_message_age" seconds, this function /// message is within "oldest_message_age" seconds, this function
/// returns true. If it's older than that, it returns false and logs a /// returns true. If it's older than that, it returns false and logs a
/// debug message. /// debug message.
fn check_message_age( fn check_message_age(
event: &SyncMessageEvent<MessageEventContent>, event: &SyncMessageLikeEvent<RoomMessageEventContent>,
oldest_message_age: u64, oldest_message_age: u64,
) -> bool { ) -> bool {
let sending_time = event let sending_time = event
.origin_server_ts .origin_server_ts()
.to_system_time() .to_system_time()
.unwrap_or(UNIX_EPOCH); .unwrap_or(UNIX_EPOCH);
@ -54,7 +47,7 @@ fn check_message_age(
/// the bot left and rejoined quickly. /// the bot left and rejoined quickly.
async fn should_process_message<'a>( async fn should_process_message<'a>(
bot: &DiceBot, bot: &DiceBot,
event: &SyncMessageEvent<MessageEventContent>, event: &SyncMessageLikeEvent<RoomMessageEventContent>,
) -> Result<(String, String), BotError> { ) -> Result<(String, String), BotError> {
//Ignore messages that are older than configured duration. //Ignore messages that are older than configured duration.
if !check_message_age(event, bot.config.oldest_message_age()) { if !check_message_age(event, bot.config.oldest_message_age()) {
@ -68,23 +61,13 @@ async fn should_process_message<'a>(
return Err(BotError::ShouldNotProcessError); return Err(BotError::ShouldNotProcessError);
} }
let (msg_body, sender_username) = if let SyncMessageEvent { let msg_body: String = event
content: .as_original()
MessageEventContent { .map(|e| e.content.body())
msgtype: MessageType::Text(TextMessageEventContent { body, .. }), .map(str::to_string)
.. .unwrap_or_else(|| String::new());
},
sender, let sender_username: String = format!("@{}:{}", event.sender().localpart(), event.sender().server_name());
..
} = event
{
(
body.clone(),
format!("@{}:{}", sender.localpart(), sender.server_name()),
)
} else {
(String::new(), String::new())
};
Ok((msg_body, sender_username)) Ok((msg_body, sender_username))
} }
@ -101,58 +84,56 @@ async fn should_process_event(db: &Database, room_id: &str, event_id: &str) -> b
}) })
} }
/// This event emitter listens for messages with dice rolling commands. pub(super) async fn on_stripped_state_member(
/// Originally adapted from the matrix-rust-sdk examples. event: StrippedStateEvent<RoomMemberEventContent>,
#[async_trait] client: Client,
impl EventHandler for DiceBot { room: Room,
async fn on_stripped_state_member( ) {
&self, let room = match room {
room: Room, Room::Invited(invited_room) => invited_room,
event: &StrippedStateEvent<MemberEventContent>, _ => return,
_: Option<MemberEventContent>, };
) {
let room = match room {
Room::Invited(invited_room) => invited_room,
_ => return,
};
if room.own_user_id().as_str() != event.state_key { if room.own_user_id().as_str() != event.state_key {
return; return;
}
info!(
"Autojoining room {}",
room.display_name().await.ok().unwrap_or_default()
);
if let Err(e) = self.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>) { info!(
let room = match room { "Autojoining room {}",
Room::Joined(joined_room) => joined_room, room.display_name().await.ok().unwrap_or_else(|| DisplayName::Named("[error]".to_string()))
_ => return, );
};
let room_id = room.room_id().as_str(); if let Err(e) = client.join_room_by_id(&room.room_id()).await {
if !should_process_event(&self.db, room_id, event.event_id.as_str()).await { warn!("Could not join room: {}", e.to_string())
return;
}
let (msg_body, sender_username) =
if let Ok((msg_body, sender_username)) = should_process_message(self, &event).await {
(msg_body, sender_username)
} else {
return;
};
let results = self
.execute_commands(&room, &sender_username, &msg_body)
.await;
self.handle_results(&room, &sender_username, event.event_id.clone(), results)
.await;
} }
} }
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(&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(&bot, &event).await {
(msg_body, sender_username)
} else {
return;
};
let results = bot
.execute_commands(&room, &sender_username, &msg_body)
.await;
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::error::BotError;
use crate::state::DiceBotState; use crate::state::DiceBotState;
use log::info; use log::info;
use matrix_sdk::{self, identifiers::EventId, room::Joined, Client, SyncSettings}; use matrix_sdk::room::Room;
use matrix_sdk::ruma::events::room::message::RoomMessageEventContent;
use matrix_sdk::ruma::events::SyncMessageLikeEvent;
use matrix_sdk::ruma::OwnedEventId;
use matrix_sdk::{self, room::Joined, Client};
use matrix_sdk::config::SyncSettings;
use std::clone::Clone; use std::clone::Clone;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
@ -18,6 +23,7 @@ const MAX_COMMANDS_PER_MESSAGE: usize = 50;
/// The DiceBot struct represents an active dice bot. The bot is not /// The DiceBot struct represents an active dice bot. The bot is not
/// connected to Matrix until its run() function is called. /// connected to Matrix until its run() function is called.
#[derive(Clone)]
pub struct DiceBot { pub struct DiceBot {
/// A reference to the configuration read in on application start. /// A reference to the configuration read in on application start.
config: Arc<Config>, config: Arc<Config>,
@ -63,12 +69,14 @@ impl DiceBot {
let device_id: Option<String> = self.db.get_device_id().await?; let device_id: Option<String> = self.db.get_device_id().await?;
let device_id: Option<&str> = device_id.as_deref(); let device_id: Option<&str> = device_id.as_deref();
client let no_device_ld_login = || client.login_username(username, password);
.login(username, password, device_id, Some("matrix dice bot")) let device_id_login = |id| client.login_username(username, password).device_id(id);
.await?; let login = device_id.map_or_else(no_device_ld_login, device_id_login);
login.send().await?;
if device_id.is_none() { if device_id.is_none() {
let device_id = client.device_id().await.ok_or(BotError::NoDeviceIdFound)?; let device_id = client.device_id().ok_or(BotError::NoDeviceIdFound)?;
self.db.set_device_id(device_id.as_str()).await?; self.db.set_device_id(device_id.as_str()).await?;
info!("Recorded new device ID: {}", device_id.as_str()); info!("Recorded new device ID: {}", device_id.as_str());
} else { } else {
@ -79,19 +87,35 @@ impl DiceBot {
Ok(()) Ok(())
} }
async fn bind_events(&self) {
//on room message: need closure to pass bot ref in.
self.client
.add_event_handler({
let bot: DiceBot = self.clone();
move |event: SyncMessageLikeEvent<RoomMessageEventContent>, room: Room| {
let bot = bot.clone();
async move { event_handlers::on_room_message(event, room, bot).await }
}
});
//auto-join handler
self.client
.add_event_handler(event_handlers::on_stripped_state_member);
}
/// Logs the bot in to Matrix and listens for events until program /// Logs the bot in to Matrix and listens for events until program
/// terminated, or a panic occurs. Originally adapted from the /// terminated, or a panic occurs. Originally adapted from the
/// matrix-rust-sdk command bot example. /// matrix-rust-sdk command bot example.
pub async fn run(self) -> Result<(), BotError> { pub async fn run(self) -> Result<(), BotError> {
let client = self.client.clone(); let client = self.client.clone();
self.login(&client).await?; self.login(&client).await?;
self.bind_events().await;
client.set_event_handler(Box::new(self)).await;
info!("Listening for commands"); info!("Listening for commands");
// TODO replace with sync_with_callback for cleaner shutdown // TODO replace with sync_with_callback for cleaner shutdown
// process. // process.
client.sync(SyncSettings::default()).await; client.sync(SyncSettings::default()).await?;
Ok(()) Ok(())
} }
@ -121,7 +145,7 @@ impl DiceBot {
&self, &self,
room: &Joined, room: &Joined,
sender_username: &str, sender_username: &str,
event_id: EventId, event_id: OwnedEventId,
results: Vec<(String, ExecutionResult)>, results: Vec<(String, ExecutionResult)>,
) { ) {
if results.len() >= 1 { if results.len() >= 1 {

View File

@ -332,7 +332,7 @@ mod tests {
macro_rules! dummy_room { macro_rules! dummy_room {
() => { () => {
crate::context::RoomContext { crate::context::RoomContext {
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"), id: &matrix_sdk::ruma::room_id!("!fakeroomid:example.com"),
display_name: "displayname".to_owned(), display_name: "displayname".to_owned(),
secure: false, secure: false,
} }
@ -485,7 +485,7 @@ mod tests {
let ctx = Context { let ctx = Context {
account: crate::models::Account::default(), account: crate::models::Account::default(),
db: db, db: db,
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(), matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: dummy_room!(), origin_room: dummy_room!(),
active_room: dummy_room!(), active_room: dummy_room!(),
username: "username", username: "username",
@ -527,7 +527,7 @@ mod tests {
let ctx = Context { let ctx = Context {
account: crate::models::Account::default(), account: crate::models::Account::default(),
db: db, db: db,
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(), matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: dummy_room!(), origin_room: dummy_room!(),
active_room: dummy_room!(), active_room: dummy_room!(),
username: "username", username: "username",
@ -566,7 +566,7 @@ mod tests {
let ctx = Context { let ctx = Context {
account: crate::models::Account::default(), account: crate::models::Account::default(),
db: db.clone(), db: db.clone(),
matrix_client: matrix_sdk::Client::new(homeserver).unwrap(), matrix_client: matrix_sdk::Client::new(homeserver).await.unwrap(),
origin_room: dummy_room!(), origin_room: dummy_room!(),
active_room: dummy_room!(), active_room: dummy_room!(),
username: "username", username: "username",

View File

@ -45,13 +45,13 @@ pub fn parse_modifiers(input: &str) -> Result<DicePoolModifiers, DiceParsingErro
let (result, rest) = parser.parse(input)?; let (result, rest) = parser.parse(input)?;
if rest.len() == 0 { if rest.len() == 0 {
convert_to_info(&result) convert_to_modifiers(&result)
} else { } else {
Err(DiceParsingError::UnconsumedInput) Err(DiceParsingError::UnconsumedInput)
} }
} }
fn convert_to_info(parsed: &Vec<ParsedInfo>) -> Result<DicePoolModifiers, DiceParsingError> { fn convert_to_modifiers(parsed: &Vec<ParsedInfo>) -> Result<DicePoolModifiers, DiceParsingError> {
use ParsedInfo::*; use ParsedInfo::*;
if parsed.len() == 0 { if parsed.len() == 0 {
Ok(DicePoolModifiers::default()) Ok(DicePoolModifiers::default())
@ -79,19 +79,8 @@ fn convert_to_info(parsed: &Vec<ParsedInfo>) -> Result<DicePoolModifiers, DicePa
} }
pub fn parse_dice_pool(input: &str) -> Result<DicePool, BotError> { pub fn parse_dice_pool(input: &str) -> Result<DicePool, BotError> {
//The "modifiers:" part is optional. Assume amounts if no modifier let (amounts, modifiers_str) = parse_amounts(input)?;
//section found.
let split = input.split(":").collect::<Vec<_>>();
let (modifiers_str, amounts_str) = (match split[..] {
[amounts] => Ok(("", amounts)),
[modifiers, amounts] => Ok((modifiers, amounts)),
_ => Err(BotError::DiceParsingError(
DiceParsingError::UnconsumedInput,
)),
})?;
let modifiers = parse_modifiers(modifiers_str)?; let modifiers = parse_modifiers(modifiers_str)?;
let amounts = parse_amounts(&amounts_str)?;
Ok(DicePool::new(amounts, modifiers)) Ok(DicePool::new(amounts, modifiers))
} }
@ -175,7 +164,7 @@ mod tests {
#[test] #[test]
fn dice_pool_number_with_quality() { fn dice_pool_number_with_quality() {
let result = parse_dice_pool("n:8"); let result = parse_dice_pool("8 n");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(
result.unwrap(), result.unwrap(),
@ -186,7 +175,7 @@ mod tests {
#[test] #[test]
fn dice_pool_number_with_success_change() { fn dice_pool_number_with_success_change() {
let modifiers = DicePoolModifiers::custom_exceptional_on(3); let modifiers = DicePoolModifiers::custom_exceptional_on(3);
let result = parse_dice_pool("s3:8"); let result = parse_dice_pool("8 s3");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), DicePool::easy_with_modifiers(8, modifiers)); assert_eq!(result.unwrap(), DicePool::easy_with_modifiers(8, modifiers));
} }
@ -194,7 +183,7 @@ mod tests {
#[test] #[test]
fn dice_pool_with_quality_and_success_change() { fn dice_pool_with_quality_and_success_change() {
let modifiers = DicePoolModifiers::custom(DicePoolQuality::Rote, 3); let modifiers = DicePoolModifiers::custom(DicePoolQuality::Rote, 3);
let result = parse_dice_pool("rs3:8"); let result = parse_dice_pool("8 rs3");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), DicePool::easy_with_modifiers(8, modifiers)); assert_eq!(result.unwrap(), DicePool::easy_with_modifiers(8, modifiers));
} }
@ -224,20 +213,20 @@ mod tests {
let expected = DicePool::new(amounts, modifiers); let expected = DicePool::new(amounts, modifiers);
let result = parse_dice_pool("rs3:8+10-2+varname"); let result = parse_dice_pool("8+10-2+varname rs3");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), expected); assert_eq!(result.unwrap(), expected);
let result = parse_dice_pool("rs3:8+10- 2 + varname"); let result = parse_dice_pool("8+10- 2 + varname rs3");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), expected); assert_eq!(result.unwrap(), expected);
let result = parse_dice_pool("rs3 : 8+ 10 -2 + varname"); let result = parse_dice_pool("8+ 10 -2 + varname rs3");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), expected); assert_eq!(result.unwrap(), expected);
//This one has tabs in it. //This one has tabs in it.
let result = parse_dice_pool(" r s3 : 8 + 10 -2 + varname"); let result = parse_dice_pool(" 8 + 10 -2 + varname r s3");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), expected); assert_eq!(result.unwrap(), expected);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,16 +4,13 @@ use crate::parser::dice::DiceParsingError;
//TOOD convert these to use parse_amounts from the common dice code. //TOOD convert these to use parse_amounts from the common dice code.
fn parse_modifier(input: &str) -> Result<DiceRollModifier, DiceParsingError> { fn parse_modifier(input: &str) -> Result<DiceRollModifier, DiceParsingError> {
if input.ends_with("bb") { match input.trim() {
Ok(DiceRollModifier::TwoBonus) "bb" => Ok(DiceRollModifier::TwoBonus),
} else if input.ends_with("b") { "b" => Ok(DiceRollModifier::OneBonus),
Ok(DiceRollModifier::OneBonus) "pp" => Ok(DiceRollModifier::TwoPenalty),
} else if input.ends_with("pp") { "p" => Ok(DiceRollModifier::OnePenalty),
Ok(DiceRollModifier::TwoPenalty) "" => Ok(DiceRollModifier::Normal),
} else if input.ends_with("p") { _ => Err(DiceParsingError::InvalidModifiers),
Ok(DiceRollModifier::OnePenalty)
} else {
Ok(DiceRollModifier::Normal)
} }
} }
@ -21,32 +18,70 @@ fn parse_modifier(input: &str) -> Result<DiceRollModifier, DiceParsingError> {
//Split based on :, send first part to parse_modifier. //Split based on :, send first part to parse_modifier.
//Send second part to parse_amounts //Send second part to parse_amounts
pub fn parse_regular_roll(input: &str) -> Result<DiceRoll, DiceParsingError> { pub fn parse_regular_roll(input: &str) -> Result<DiceRoll, DiceParsingError> {
let input: Vec<&str> = input.trim().split(":").collect(); let (amount, modifiers_str) = crate::parser::dice::parse_single_amount(input)?;
let (modifiers_str, amounts_str) = match input[..] {
[amounts] => Ok(("", amounts)),
[modifiers, amounts] => Ok((modifiers, amounts)),
_ => Err(DiceParsingError::UnconsumedInput),
}?;
let modifier = parse_modifier(modifiers_str)?; let modifier = parse_modifier(modifiers_str)?;
let amount = crate::parser::dice::parse_single_amount(amounts_str)?;
Ok(DiceRoll { modifier, amount }) Ok(DiceRoll { modifier, amount })
} }
pub fn parse_advancement_roll(input: &str) -> Result<AdvancementRoll, DiceParsingError> { pub fn parse_advancement_roll(input: &str) -> Result<AdvancementRoll, DiceParsingError> {
let input = input.trim(); let input = input.trim();
let amounts = crate::parser::dice::parse_single_amount(input)?; let (amounts, unconsumed_input) = crate::parser::dice::parse_single_amount(input)?;
Ok(AdvancementRoll { if unconsumed_input.len() == 0 {
existing_skill: amounts, Ok(AdvancementRoll {
}) existing_skill: amounts,
})
} else {
Err(DiceParsingError::InvalidAmount)
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::parser::dice::{Amount, Element, Operator}; use crate::parser::dice::{Amount, DiceParsingError, Element, Operator};
#[test]
fn parse_modifier_rejects_bad_value() {
let modifier = parse_modifier("qqq");
assert!(matches!(modifier, Err(DiceParsingError::InvalidModifiers)))
}
#[test]
fn parse_modifier_accepts_one_bonus() {
let modifier = parse_modifier("b");
assert!(matches!(modifier, Ok(DiceRollModifier::OneBonus)))
}
#[test]
fn parse_modifier_accepts_two_bonus() {
let modifier = parse_modifier("bb");
assert!(matches!(modifier, Ok(DiceRollModifier::TwoBonus)))
}
#[test]
fn parse_modifier_accepts_two_penalty() {
let modifier = parse_modifier("pp");
assert!(matches!(modifier, Ok(DiceRollModifier::TwoPenalty)))
}
#[test]
fn parse_modifier_accepts_one_penalty() {
let modifier = parse_modifier("p");
assert!(matches!(modifier, Ok(DiceRollModifier::OnePenalty)))
}
#[test]
fn parse_modifier_accepts_normal() {
let modifier = parse_modifier("");
assert!(matches!(modifier, Ok(DiceRollModifier::Normal)))
}
#[test]
fn parse_modifier_accepts_normal_unaffected_by_whitespace() {
let modifier = parse_modifier(" ");
assert!(matches!(modifier, Ok(DiceRollModifier::Normal)))
}
#[test] #[test]
fn regular_roll_accepts_single_number() { fn regular_roll_accepts_single_number() {
@ -72,7 +107,7 @@ mod tests {
#[test] #[test]
fn regular_roll_accepts_two_bonus() { fn regular_roll_accepts_two_bonus() {
let result = parse_regular_roll("bb:60"); let result = parse_regular_roll("60 bb");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(
DiceRoll { DiceRoll {
@ -88,7 +123,7 @@ mod tests {
#[test] #[test]
fn regular_roll_accepts_one_bonus() { fn regular_roll_accepts_one_bonus() {
let result = parse_regular_roll("b:60"); let result = parse_regular_roll("60 b");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(
DiceRoll { DiceRoll {
@ -104,7 +139,7 @@ mod tests {
#[test] #[test]
fn regular_roll_accepts_two_penalty() { fn regular_roll_accepts_two_penalty() {
let result = parse_regular_roll("pp:60"); let result = parse_regular_roll("60 pp");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(
DiceRoll { DiceRoll {
@ -120,7 +155,7 @@ mod tests {
#[test] #[test]
fn regular_roll_accepts_one_penalty() { fn regular_roll_accepts_one_penalty() {
let result = parse_regular_roll("p:60"); let result = parse_regular_roll("60 p");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(
DiceRoll { DiceRoll {
@ -140,21 +175,21 @@ mod tests {
assert!(parse_regular_roll(" 60").is_ok()); assert!(parse_regular_roll(" 60").is_ok());
assert!(parse_regular_roll(" 60 ").is_ok()); assert!(parse_regular_roll(" 60 ").is_ok());
assert!(parse_regular_roll("bb:60 ").is_ok()); assert!(parse_regular_roll("60bb ").is_ok());
assert!(parse_regular_roll(" bb:60").is_ok()); assert!(parse_regular_roll(" 60 bb").is_ok());
assert!(parse_regular_roll(" bb:60 ").is_ok()); assert!(parse_regular_roll(" 60 bb ").is_ok());
assert!(parse_regular_roll("b:60 ").is_ok()); assert!(parse_regular_roll("60b ").is_ok());
assert!(parse_regular_roll(" b:60").is_ok()); assert!(parse_regular_roll(" 60 b").is_ok());
assert!(parse_regular_roll(" b:60 ").is_ok()); assert!(parse_regular_roll(" 60 b ").is_ok());
assert!(parse_regular_roll("pp:60 ").is_ok()); assert!(parse_regular_roll("60pp ").is_ok());
assert!(parse_regular_roll(" pp:60").is_ok()); assert!(parse_regular_roll(" 60 pp").is_ok());
assert!(parse_regular_roll(" pp:60 ").is_ok()); assert!(parse_regular_roll(" 60 pp ").is_ok());
assert!(parse_regular_roll("p:60 ").is_ok()); assert!(parse_regular_roll("60p ").is_ok());
assert!(parse_regular_roll(" p:60").is_ok()); assert!(parse_regular_roll(" 60p ").is_ok());
assert!(parse_regular_roll(" p:60 ").is_ok()); assert!(parse_regular_roll(" 60 p ").is_ok());
} }
#[test] #[test]

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

View File

@ -53,34 +53,41 @@ impl Rooms for Database {
mod tests { mod tests {
use crate::db::sqlite::Database; use crate::db::sqlite::Database;
use crate::db::Rooms; use crate::db::Rooms;
use std::future::Future;
async fn create_db() -> Database { async fn with_db<Fut>(f: impl FnOnce(Database) -> Fut)
where
Fut: Future<Output = ()>,
{
let db_path = tempfile::NamedTempFile::new_in(".").unwrap(); let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap()) crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
.await .await
.unwrap(); .unwrap();
Database::new(db_path.path().to_str().unwrap()) let db = Database::new(db_path.path().to_str().unwrap())
.await .await
.unwrap() .unwrap();
f(db).await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn should_process_test() { async fn should_process_test() {
let db = create_db().await; with_db(|db| async move {
let first_check = db
.should_process("myroom", "myeventid")
.await
.expect("should_process failed in first insert");
let first_check = db assert_eq!(first_check, true);
.should_process("myroom", "myeventid")
.await
.expect("should_process failed in first insert");
assert_eq!(first_check, true); let second_check = db
.should_process("myroom", "myeventid")
.await
.expect("should_process failed in first insert");
let second_check = db assert_eq!(second_check, false);
.should_process("myroom", "myeventid") })
.await .await;
.expect("should_process failed in first insert");
assert_eq!(second_check, false);
} }
} }

View File

@ -37,54 +37,64 @@ impl DbState for Database {
mod tests { mod tests {
use crate::db::sqlite::Database; use crate::db::sqlite::Database;
use crate::db::DbState; use crate::db::DbState;
use std::future::Future;
async fn create_db() -> Database { async fn with_db<Fut>(f: impl FnOnce(Database) -> Fut)
where
Fut: Future<Output = ()>,
{
let db_path = tempfile::NamedTempFile::new_in(".").unwrap(); let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap()) crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
.await .await
.unwrap(); .unwrap();
Database::new(db_path.path().to_str().unwrap()) let db = Database::new(db_path.path().to_str().unwrap())
.await .await
.unwrap() .unwrap();
f(db).await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn set_and_get_device_id() { async fn set_and_get_device_id() {
let db = create_db().await; with_db(|db| async move {
db.set_device_id("device_id")
.await
.expect("Could not set device ID");
db.set_device_id("device_id") let device_id = db.get_device_id().await.expect("Could not get device ID");
.await
.expect("Could not set device ID");
let device_id = db.get_device_id().await.expect("Could not get device ID"); assert!(device_id.is_some());
assert_eq!(device_id.unwrap(), "device_id");
assert!(device_id.is_some()); })
assert_eq!(device_id.unwrap(), "device_id"); .await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn no_device_id_set_returns_none() { async fn no_device_id_set_returns_none() {
let db = create_db().await; with_db(|db| async move {
let device_id = db.get_device_id().await.expect("Could not get device ID"); let device_id = db.get_device_id().await.expect("Could not get device ID");
assert!(device_id.is_none()); assert!(device_id.is_none());
})
.await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_update_device_id() { async fn can_update_device_id() {
let db = create_db().await; with_db(|db| async move {
db.set_device_id("device_id")
.await
.expect("Could not set device ID");
db.set_device_id("device_id") db.set_device_id("device_id2")
.await .await
.expect("Could not set device ID"); .expect("Could not set device ID");
db.set_device_id("device_id2") let device_id = db.get_device_id().await.expect("Could not get device ID");
.await
.expect("Could not set device ID");
let device_id = db.get_device_id().await.expect("Could not get device ID"); assert!(device_id.is_some());
assert_eq!(device_id.unwrap(), "device_id2");
assert!(device_id.is_some()); })
assert_eq!(device_id.unwrap(), "device_id2"); .await;
} }
} }

View File

@ -91,251 +91,271 @@ mod tests {
use crate::db::sqlite::Database; use crate::db::sqlite::Database;
use crate::db::Users; use crate::db::Users;
use crate::models::AccountStatus; use crate::models::AccountStatus;
use std::future::Future;
async fn create_db() -> Database { async fn with_db<Fut>(f: impl FnOnce(Database) -> Fut)
where
Fut: Future<Output = ()>,
{
let db_path = tempfile::NamedTempFile::new_in(".").unwrap(); let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap()) crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
.await .await
.unwrap(); .unwrap();
Database::new(db_path.path().to_str().unwrap()) let db = Database::new(db_path.path().to_str().unwrap())
.await .await
.unwrap() .unwrap();
f(db).await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn create_and_get_full_user_test() { async fn create_and_get_full_user_test() {
let db = create_db().await; with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
username: "myuser".to_string(),
password: Some("abc".to_string()),
account_status: AccountStatus::Registered,
active_room: Some("myroom".to_string()),
})
.await;
let insert_result = db assert!(insert_result.is_ok());
.upsert_user(&User {
username: "myuser".to_string(),
password: Some("abc".to_string()),
account_status: AccountStatus::Registered,
active_room: Some("myroom".to_string()),
})
.await;
assert!(insert_result.is_ok()); let user = db
.get_user("myuser")
.await
.expect("User retrieval query failed");
let user = db assert!(user.is_some());
.get_user("myuser") let user = user.unwrap();
.await assert_eq!(user.username, "myuser");
.expect("User retrieval query failed"); assert_eq!(user.password, Some("abc".to_string()));
assert_eq!(user.account_status, AccountStatus::Registered);
assert!(user.is_some()); assert_eq!(user.active_room, Some("myroom".to_string()));
let user = user.unwrap(); })
assert_eq!(user.username, "myuser"); .await;
assert_eq!(user.password, Some("abc".to_string()));
assert_eq!(user.account_status, AccountStatus::Registered);
assert_eq!(user.active_room, Some("myroom".to_string()));
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_get_user_with_no_state_record() { async fn can_get_user_with_no_state_record() {
let db = create_db().await; with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
username: "myuser".to_string(),
password: Some("abc".to_string()),
account_status: AccountStatus::AwaitingActivation,
active_room: Some("myroom".to_string()),
})
.await;
let insert_result = db assert!(insert_result.is_ok());
.upsert_user(&User {
username: "myuser".to_string(),
password: Some("abc".to_string()),
account_status: AccountStatus::AwaitingActivation,
active_room: Some("myroom".to_string()),
})
.await;
assert!(insert_result.is_ok()); sqlx::query("DELETE FROM user_state")
.execute(&db.conn)
.await
.expect("Could not delete from user_state table.");
sqlx::query("DELETE FROM user_state") let user = db
.execute(&db.conn) .get_user("myuser")
.await .await
.expect("Could not delete from user_state table."); .expect("User retrieval query failed");
let user = db assert!(user.is_some());
.get_user("myuser") let user = user.unwrap();
.await assert_eq!(user.username, "myuser");
.expect("User retrieval query failed"); assert_eq!(user.password, Some("abc".to_string()));
assert_eq!(user.account_status, AccountStatus::AwaitingActivation);
assert!(user.is_some()); //These should be default values because the state record is missing.
let user = user.unwrap(); assert_eq!(user.active_room, None);
assert_eq!(user.username, "myuser"); })
assert_eq!(user.password, Some("abc".to_string())); .await;
assert_eq!(user.account_status, AccountStatus::AwaitingActivation);
//These should be default values because the state record is missing.
assert_eq!(user.active_room, None);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_insert_without_password() { async fn can_insert_without_password() {
let db = create_db().await; with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
username: "myuser".to_string(),
password: None,
..Default::default()
})
.await;
let insert_result = db assert!(insert_result.is_ok());
.upsert_user(&User {
username: "myuser".to_string(),
password: None,
..Default::default()
})
.await;
assert!(insert_result.is_ok()); let user = db
.get_user("myuser")
.await
.expect("User retrieval query failed");
let user = db assert!(user.is_some());
.get_user("myuser") let user = user.unwrap();
.await assert_eq!(user.username, "myuser");
.expect("User retrieval query failed"); assert_eq!(user.password, None);
})
assert!(user.is_some()); .await;
let user = user.unwrap();
assert_eq!(user.username, "myuser");
assert_eq!(user.password, None);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_insert_without_active_room() { async fn can_insert_without_active_room() {
let db = create_db().await; with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
username: "myuser".to_string(),
active_room: None,
..Default::default()
})
.await;
let insert_result = db assert!(insert_result.is_ok());
.upsert_user(&User {
username: "myuser".to_string(),
active_room: None,
..Default::default()
})
.await;
assert!(insert_result.is_ok()); let user = db
.get_user("myuser")
.await
.expect("User retrieval query failed");
let user = db assert!(user.is_some());
.get_user("myuser") let user = user.unwrap();
.await assert_eq!(user.username, "myuser");
.expect("User retrieval query failed"); assert_eq!(user.active_room, None);
})
assert!(user.is_some()); .await;
let user = user.unwrap();
assert_eq!(user.username, "myuser");
assert_eq!(user.active_room, None);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_update_user() { async fn can_update_user() {
let db = create_db().await; with_db(|db| async move {
let insert_result1 = db
.upsert_user(&User {
username: "myuser".to_string(),
password: Some("abc".to_string()),
..Default::default()
})
.await;
let insert_result1 = db assert!(insert_result1.is_ok());
.upsert_user(&User {
username: "myuser".to_string(),
password: Some("abc".to_string()),
..Default::default()
})
.await;
assert!(insert_result1.is_ok()); let insert_result2 = db
.upsert_user(&User {
username: "myuser".to_string(),
password: Some("123".to_string()),
active_room: Some("room".to_string()),
account_status: AccountStatus::AwaitingActivation,
})
.await;
let insert_result2 = db assert!(insert_result2.is_ok());
.upsert_user(&User {
username: "myuser".to_string(),
password: Some("123".to_string()),
active_room: Some("room".to_string()),
account_status: AccountStatus::AwaitingActivation,
})
.await;
assert!(insert_result2.is_ok()); let user = db
.get_user("myuser")
.await
.expect("User retrieval query failed");
let user = db assert!(user.is_some());
.get_user("myuser") let user = user.unwrap();
.await assert_eq!(user.username, "myuser");
.expect("User retrieval query failed");
assert!(user.is_some()); //From second upsert
let user = user.unwrap(); assert_eq!(user.password, Some("123".to_string()));
assert_eq!(user.username, "myuser"); assert_eq!(user.active_room, Some("room".to_string()));
assert_eq!(user.account_status, AccountStatus::AwaitingActivation);
//From second upsert })
assert_eq!(user.password, Some("123".to_string())); .await;
assert_eq!(user.active_room, Some("room".to_string()));
assert_eq!(user.account_status, AccountStatus::AwaitingActivation);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_delete_user() { async fn can_delete_user() {
let db = create_db().await; with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
username: "myuser".to_string(),
password: Some("abc".to_string()),
..Default::default()
})
.await;
let insert_result = db assert!(insert_result.is_ok());
.upsert_user(&User {
username: "myuser".to_string(),
password: Some("abc".to_string()),
..Default::default()
})
.await;
assert!(insert_result.is_ok()); db.delete_user("myuser")
.await
.expect("User deletion query failed");
db.delete_user("myuser") let user = db
.await .get_user("myuser")
.expect("User deletion query failed"); .await
.expect("User retrieval query failed");
let user = db assert!(user.is_none());
.get_user("myuser") })
.await .await;
.expect("User retrieval query failed");
assert!(user.is_none());
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn username_not_in_db_returns_none() { async fn username_not_in_db_returns_none() {
let db = create_db().await; with_db(|db| async move {
let user = db let user = db
.get_user("does not exist") .get_user("does not exist")
.await .await
.expect("Get user query failure"); .expect("Get user query failure");
assert!(user.is_none()); assert!(user.is_none());
})
.await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn authenticate_user_is_some_with_valid_password() { async fn authenticate_user_is_some_with_valid_password() {
let db = create_db().await; with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
username: "myuser".to_string(),
password: Some(
crate::logic::hash_password("abc").expect("password hash error!"),
),
..Default::default()
})
.await;
let insert_result = db assert!(insert_result.is_ok());
.upsert_user(&User {
username: "myuser".to_string(),
password: Some(crate::logic::hash_password("abc").expect("password hash error!")),
..Default::default()
})
.await;
assert!(insert_result.is_ok()); let user = db
.authenticate_user("myuser", "abc")
.await
.expect("User retrieval query failed");
let user = db assert!(user.is_some());
.authenticate_user("myuser", "abc") let user = user.unwrap();
.await assert_eq!(user.username, "myuser");
.expect("User retrieval query failed"); })
.await;
assert!(user.is_some());
let user = user.unwrap();
assert_eq!(user.username, "myuser");
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn authenticate_user_is_none_with_wrong_password() { async fn authenticate_user_is_none_with_wrong_password() {
let db = create_db().await; with_db(|db| async move {
let insert_result = db
.upsert_user(&User {
username: "myuser".to_string(),
password: Some(
crate::logic::hash_password("abc").expect("password hash error!"),
),
..Default::default()
})
.await;
let insert_result = db assert!(insert_result.is_ok());
.upsert_user(&User {
username: "myuser".to_string(),
password: Some(crate::logic::hash_password("abc").expect("password hash error!")),
..Default::default()
})
.await;
assert!(insert_result.is_ok()); let user = db
.authenticate_user("myuser", "wrong-password")
.await
.expect("User retrieval query failed");
let user = db assert!(user.is_none());
.authenticate_user("myuser", "wrong-password") })
.await .await;
.expect("User retrieval query failed");
assert!(user.is_none());
} }
} }

View File

@ -102,143 +102,156 @@ mod tests {
use super::*; use super::*;
use crate::db::sqlite::Database; use crate::db::sqlite::Database;
use crate::db::Variables; use crate::db::Variables;
use std::future::Future;
async fn create_db() -> Database { async fn with_db<Fut>(f: impl FnOnce(Database) -> Fut)
where
Fut: Future<Output = ()>,
{
let db_path = tempfile::NamedTempFile::new_in(".").unwrap(); let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap()) crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
.await .await
.unwrap(); .unwrap();
Database::new(db_path.path().to_str().unwrap()) let db = Database::new(db_path.path().to_str().unwrap())
.await .await
.unwrap() .unwrap();
f(db).await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn set_and_get_variable_test() { async fn set_and_get_variable_test() {
let db = create_db().await; with_db(|db| async move {
db.set_user_variable("myuser", "myroom", "myvariable", 1)
.await
.expect("Could not set variable");
db.set_user_variable("myuser", "myroom", "myvariable", 1) let value = db
.await .get_user_variable("myuser", "myroom", "myvariable")
.expect("Could not set variable"); .await
.expect("Could not get variable");
let value = db assert_eq!(value, 1);
.get_user_variable("myuser", "myroom", "myvariable") })
.await .await;
.expect("Could not get variable");
assert_eq!(value, 1);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn get_missing_variable_test() { async fn get_missing_variable_test() {
let db = create_db().await; with_db(|db| async move {
let value = db.get_user_variable("myuser", "myroom", "myvariable").await;
let value = db.get_user_variable("myuser", "myroom", "myvariable").await; assert!(value.is_err());
assert!(matches!(
assert!(value.is_err()); value.err().unwrap(),
assert!(matches!( DataError::KeyDoesNotExist(_)
value.err().unwrap(), ));
DataError::KeyDoesNotExist(_) })
)); .await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn get_other_user_variable_test() { async fn get_other_user_variable_test() {
let db = create_db().await; with_db(|db| async move {
db.set_user_variable("myuser1", "myroom", "myvariable", 1)
.await
.expect("Could not set variable");
db.set_user_variable("myuser1", "myroom", "myvariable", 1) let value = db
.await .get_user_variable("myuser2", "myroom", "myvariable")
.expect("Could not set variable"); .await;
let value = db assert!(value.is_err());
.get_user_variable("myuser2", "myroom", "myvariable") assert!(matches!(
.await; value.err().unwrap(),
DataError::KeyDoesNotExist(_)
assert!(value.is_err()); ));
assert!(matches!( })
value.err().unwrap(), .await;
DataError::KeyDoesNotExist(_)
));
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn count_variables_test() { async fn count_variables_test() {
let db = create_db().await; with_db(|db| async move {
for variable_name in &["var1", "var2", "var3"] {
db.set_user_variable("myuser", "myroom", variable_name, 1)
.await
.expect("Could not set variable");
}
for variable_name in &["var1", "var2", "var3"] { let count = db
db.set_user_variable("myuser", "myroom", variable_name, 1) .get_variable_count("myuser", "myroom")
.await .await
.expect("Could not set variable"); .expect("Could not get count.");
}
let count = db assert_eq!(count, 3);
.get_variable_count("myuser", "myroom") })
.await .await;
.expect("Could not get count.");
assert_eq!(count, 3);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn count_variables_respects_user_id() { async fn count_variables_respects_user_id() {
let db = create_db().await; with_db(|db| async move {
for variable_name in &["var1", "var2", "var3"] {
db.set_user_variable("different-user", "myroom", variable_name, 1)
.await
.expect("Could not set variable");
}
for variable_name in &["var1", "var2", "var3"] { let count = db
db.set_user_variable("different-user", "myroom", variable_name, 1) .get_variable_count("myuser", "myroom")
.await .await
.expect("Could not set variable"); .expect("Could not get count.");
}
let count = db assert_eq!(count, 0);
.get_variable_count("myuser", "myroom") })
.await .await;
.expect("Could not get count.");
assert_eq!(count, 0);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn count_variables_respects_room_id() { async fn count_variables_respects_room_id() {
let db = create_db().await; with_db(|db| async move {
for variable_name in &["var1", "var2", "var3"] {
db.set_user_variable("myuser", "different-room", variable_name, 1)
.await
.expect("Could not set variable");
}
for variable_name in &["var1", "var2", "var3"] { let count = db
db.set_user_variable("myuser", "different-room", variable_name, 1) .get_variable_count("myuser", "myroom")
.await .await
.expect("Could not set variable"); .expect("Could not get count.");
}
let count = db assert_eq!(count, 0);
.get_variable_count("myuser", "myroom") })
.await .await;
.expect("Could not get count.");
assert_eq!(count, 0);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn delete_variable_test() { async fn delete_variable_test() {
let db = create_db().await; with_db(|db| async move {
for variable_name in &["var1", "var2", "var3"] {
db.set_user_variable("myuser", "myroom", variable_name, 1)
.await
.expect("Could not set variable");
}
for variable_name in &["var1", "var2", "var3"] { db.delete_user_variable("myuser", "myroom", "var1")
db.set_user_variable("myuser", "myroom", variable_name, 1)
.await .await
.expect("Could not set variable"); .expect("Could not delete variable.");
}
db.delete_user_variable("myuser", "myroom", "var1") let count = db
.await .get_variable_count("myuser", "myroom")
.expect("Could not delete variable."); .await
.expect("Could not get count");
let count = db assert_eq!(count, 2);
.get_variable_count("myuser", "myroom")
.await
.expect("Could not get count");
assert_eq!(count, 2); let var1 = db.get_user_variable("myuser", "myroom", "var1").await;
assert!(var1.is_err());
let var1 = db.get_user_variable("myuser", "myroom", "var1").await; assert!(matches!(var1.err().unwrap(), DataError::KeyDoesNotExist(_)));
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")] #[error("could not retrieve device id")]
NoDeviceIdFound, NoDeviceIdFound,
#[error("could not build client: {0}")]
ClientBuildError(#[from] matrix_sdk::ClientBuildError),
#[error("could not open matrix store: {0}")]
OpenStoreError(#[from] matrix_sdk::store::OpenStoreError),
#[error("command error: {0}")] #[error("command error: {0}")]
CommandError(#[from] CommandError), CommandError(#[from] CommandError),
@ -33,15 +39,15 @@ pub enum BotError {
#[error("could not parse URL")] #[error("could not parse URL")]
UrlParseError(#[from] url::ParseError), UrlParseError(#[from] url::ParseError),
#[error("could not parse ID")]
IdParseError(#[from] matrix_sdk::ruma::IdParseError),
#[error("error in matrix state store: {0}")] #[error("error in matrix state store: {0}")]
MatrixStateStoreError(#[from] matrix_sdk::StoreError), MatrixStateStoreError(#[from] matrix_sdk::StoreError),
#[error("uncategorized matrix SDK error: {0}")] #[error("uncategorized matrix SDK error: {0}")]
MatrixError(#[from] matrix_sdk::Error), MatrixError(#[from] matrix_sdk::Error),
#[error("uncategorized matrix SDK base error: {0}")]
MatrixBaseError(#[from] matrix_sdk::BaseError),
#[error("future canceled")] #[error("future canceled")]
FutureCanceledError, FutureCanceledError,
@ -79,8 +85,8 @@ pub enum BotError {
#[error("could not convert to proper integer type")] #[error("could not convert to proper integer type")]
TryFromIntError(#[from] std::num::TryFromIntError), TryFromIntError(#[from] std::num::TryFromIntError),
#[error("identifier error: {0}")] // #[error("identifier error: {0}")]
IdentifierError(#[from] matrix_sdk::identifiers::Error), // IdentifierError(#[from] matrix_sdk::ruma::Error),
#[error("password creation error: {0}")] #[error("password creation error: {0}")]
PasswordCreationError(argon2::Error), PasswordCreationError(argon2::Error),

View File

@ -6,6 +6,9 @@ pub fn parse_help_topic(input: &str) -> Option<HelpTopic> {
"dicepool" => Some(HelpTopic::DicePool), "dicepool" => Some(HelpTopic::DicePool),
"dice" => Some(HelpTopic::RollingDice), "dice" => Some(HelpTopic::RollingDice),
"cthulhu" => Some(HelpTopic::Cthulhu), "cthulhu" => Some(HelpTopic::Cthulhu),
"variables" => Some(HelpTopic::Variables),
"var" => Some(HelpTopic::Variables),
"variable" => Some(HelpTopic::Variables),
"" => Some(HelpTopic::General), "" => Some(HelpTopic::General),
_ => None, _ => None,
} }
@ -16,6 +19,7 @@ pub enum HelpTopic {
DicePool, DicePool,
Cthulhu, Cthulhu,
RollingDice, RollingDice,
Variables,
General, General,
} }
@ -101,6 +105,34 @@ Note: If !cthadv is given a variable, and the roll is successful, it will
update the variable with the new skill. update the variable with the new skill.
"}; "};
const VARIABLES_HELP: &'static str = indoc! {"
Variables
Commands: !get, !set, !variables
Manage variables that can be substituted into roll commands.
Examples: !get myvar, !set myvar 10
!get <variable> = show variable of the given name
!set <variable> <num> = set a variable to a number
The !variables command will list all variables for the room. The
variables command cna be used in a secure room to avoid spamming the
actual room that the variable is set in.
Variable names can be used in all types of dice rolls:
!pool myvar + 3
!roll myvar
There are some limitations on variables: they cannot themselves be
dice expressions (i.e. can only be numbers), and they must be uniquely
parseable in an expression (i.e 'myvard6' does not work for the !roll
command).
"};
const GENERAL_HELP: &'static str = indoc! {" const GENERAL_HELP: &'static str = indoc! {"
General Help General Help
@ -117,6 +149,7 @@ impl HelpTopic {
HelpTopic::DicePool => DICEPOOL_HELP, HelpTopic::DicePool => DICEPOOL_HELP,
HelpTopic::Cthulhu => CTHULHU_HELP, HelpTopic::Cthulhu => CTHULHU_HELP,
HelpTopic::RollingDice => DICE_HELP, HelpTopic::RollingDice => DICE_HELP,
HelpTopic::Variables => VARIABLES_HELP,
HelpTopic::General => GENERAL_HELP, HelpTopic::General => GENERAL_HELP,
} }
} }

View File

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

View File

@ -71,53 +71,61 @@ mod tests {
use super::*; use super::*;
use crate::db::Users; use crate::db::Users;
use crate::models::{AccountStatus, User}; use crate::models::{AccountStatus, User};
use std::future::Future;
async fn create_db() -> Database { async fn with_db<Fut>(f: impl FnOnce(Database) -> Fut)
where
Fut: Future<Output = ()>,
{
let db_path = tempfile::NamedTempFile::new_in(".").unwrap(); let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap()) crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
.await .await
.unwrap(); .unwrap();
Database::new(db_path.path().to_str().unwrap()) let db = Database::new(db_path.path().to_str().unwrap())
.await .await
.unwrap() .unwrap();
f(db).await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn get_account_no_user_exists() { async fn get_account_no_user_exists() {
let db = create_db().await; with_db(|db| async move {
let account = get_account(&db, "@test:example.com")
.await
.expect("Account retrieval didn't work");
let account = get_account(&db, "@test:example.com") assert!(matches!(account, Account::Transient(_)));
.await
.expect("Account retrieval didn't work");
assert!(matches!(account, Account::Transient(_))); let user = account.transient_user().unwrap();
assert_eq!(user.username, "@test:example.com");
let user = account.transient_user().unwrap(); })
assert_eq!(user.username, "@test:example.com"); .await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn get_or_create_user_when_user_exists() { async fn get_or_create_user_when_user_exists() {
let db = create_db().await; with_db(|db| async move {
let user = User {
username: "myuser".to_string(),
password: Some("abc".to_string()),
account_status: AccountStatus::Registered,
active_room: Some("myroom".to_string()),
};
let user = User { let insert_result = db.upsert_user(&user).await;
username: "myuser".to_string(), assert!(insert_result.is_ok());
password: Some("abc".to_string()),
account_status: AccountStatus::Registered,
active_room: Some("myroom".to_string()),
};
let insert_result = db.upsert_user(&user).await; let account = get_account(&db, "myuser")
assert!(insert_result.is_ok()); .await
.expect("Account retrieval did not work");
let account = get_account(&db, "myuser") assert!(matches!(account, Account::Registered(_)));
.await
.expect("Account retrieval did not work");
assert!(matches!(account, Account::Registered(_))); let user_again = account.registered_user().unwrap();
assert_eq!(user, *user_again);
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 futures::stream::{self, StreamExt, TryStreamExt};
use log::error; use log::error;
use matrix_sdk::{events::room::message::NoticeMessageEventContent, room::Joined, ClientConfig}; use matrix_sdk::ruma::events::room::message::{InReplyTo, RoomMessageEventContent, Relation};
use matrix_sdk::{ use matrix_sdk::ruma::events::AnyMessageLikeEventContent;
events::room::message::{InReplyTo, Relation}, use matrix_sdk::ruma::{RoomId, OwnedEventId, OwnedUserId};
events::room::message::{MessageEventContent, MessageType}, use matrix_sdk::Client;
events::AnyMessageEventContent, use matrix_sdk::Error as MatrixError;
identifiers::EventId, use matrix_sdk::room::Joined;
Error as MatrixError,
};
use matrix_sdk::{identifiers::RoomId, identifiers::UserId, Client};
use url::Url; use url::Url;
use crate::{config::Config, error::BotError}; use crate::{config::Config, error::BotError};
@ -32,12 +29,16 @@ fn extract_error_message(error: MatrixError) -> String {
} }
/// Creates the matrix client. /// Creates the matrix client.
pub fn create_client(config: &Config) -> Result<Client, BotError> { pub async fn create_client(config: &Config) -> Result<Client, BotError> {
let cache_dir = cache_dir()?; let cache_dir = cache_dir()?;
let client_config = ClientConfig::new().store_path(cache_dir);
let homeserver_url = Url::parse(&config.matrix_homeserver())?; let homeserver_url = Url::parse(&config.matrix_homeserver())?;
Ok(Client::new_with_config(homeserver_url, client_config)?) let client = Client::builder()
.sled_store(cache_dir, None)?
.homeserver_url(homeserver_url).build()
.await?;
Ok(client)
} }
/// Retrieve a list of users in a given room. /// Retrieve a list of users in a given room.
@ -59,7 +60,7 @@ pub async fn get_users_in_room(
pub async fn get_rooms_for_user( pub async fn get_rooms_for_user(
client: &Client, client: &Client,
user: &UserId, user: &OwnedUserId,
) -> Result<Vec<Joined>, MatrixError> { ) -> Result<Vec<Joined>, MatrixError> {
// Carries errors through, in case we cannot load joined user IDs // Carries errors through, in case we cannot load joined user IDs
// from the room for some reason. // from the room for some reason.
@ -87,7 +88,7 @@ pub async fn send_message(
client: &Client, client: &Client,
room_id: &RoomId, room_id: &RoomId,
message: (&str, &str), message: (&str, &str),
reply_to: Option<EventId>, reply_to: Option<OwnedEventId>,
) { ) {
let (html, plain) = message; let (html, plain) = message;
let room = match client.get_joined_room(room_id) { let room = match client.get_joined_room(room_id) {
@ -95,15 +96,13 @@ pub async fn send_message(
_ => return, _ => return,
}; };
let mut content = MessageEventContent::new(MessageType::Notice( let mut content = RoomMessageEventContent::notice_html(plain.trim(), html);
NoticeMessageEventContent::html(plain.trim(), html),
));
content.relates_to = reply_to.map(|event_id| Relation::Reply { content.relates_to = reply_to.map(|event_id| Relation::Reply {
in_reply_to: InReplyTo::new(event_id), in_reply_to: InReplyTo::new(event_id)
}); });
let content = AnyMessageEventContent::RoomMessage(content); let content = AnyMessageLikeEventContent::RoomMessage(content);
let result = room.send(content, None).await; let result = room.send(content, None).await;

View File

@ -151,8 +151,9 @@ where
/// should not have an operator, but every one after that should. /// should not have an operator, but every one after that should.
/// Accepts expressions like "8", "10 + variablename", "variablename - /// Accepts expressions like "8", "10 + variablename", "variablename -
/// 3", etc. This function is currently common to systems that don't /// 3", etc. This function is currently common to systems that don't
/// deal with XdY rolls. Support for that will be added later. /// deal with XdY rolls. Support for that will be added later. Returns
pub fn parse_amounts(input: &str) -> ParseResult<Vec<Amount>> { /// parsed amounts and unconsumed input (e.g. roll modifiers).
pub fn parse_amounts(input: &str) -> ParseResult<(Vec<Amount>, &str)> {
let input = input.trim(); let input = input.trim();
let remaining_amounts = many(amount_parser()).map(|amounts: Vec<ParseResult<Amount>>| amounts); let remaining_amounts = many(amount_parser()).map(|amounts: Vec<ParseResult<Amount>>| amounts);
@ -169,31 +170,23 @@ pub fn parse_amounts(input: &str) -> ParseResult<Vec<Amount>> {
(amounts, results.1) (amounts, results.1)
})?; })?;
if rest.len() == 0 { // Any ParseResult errors will short-circuit the collect.
// Any ParseResult errors will short-circuit the collect. let results: Vec<Amount> = results.into_iter().collect::<ParseResult<_>>()?;
results.into_iter().collect() Ok((results, rest))
} else {
Err(DiceParsingError::UnconsumedInput)
}
} }
/// Parse an expression that expects a single number or variable. No /// Parse an expression that expects a single number or variable. No
/// operators are allowed. This function is common to systems that /// operators are allowed. This function is common to systems that
/// don't deal with XdY rolls. Currently. this function does not /// don't deal with XdY rolls. Currently. this function does not
/// support parsing negative numbers. /// support parsing negative numbers. Returns the parsed amount and
pub fn parse_single_amount(input: &str) -> ParseResult<Amount> { /// any unconsumed input (useful for dice roll modifiers).
pub fn parse_single_amount(input: &str) -> ParseResult<(Amount, &str)> {
// TODO add support for negative numbers, as technically they // TODO add support for negative numbers, as technically they
// should be allowed. // should be allowed.
let input = input.trim(); let input = input.trim();
let mut parser = first_amount_parser().map(|amount: ParseResult<Amount>| amount); let mut parser = first_amount_parser().map(|amount: ParseResult<Amount>| amount);
let (result, rest) = parser.parse(input)?; let (result, rest) = parser.parse(input)?;
Ok((result?, rest))
if rest.len() == 0 {
result
} else {
Err(DiceParsingError::UnconsumedInput)
}
} }
#[cfg(test)] #[cfg(test)]
@ -206,10 +199,13 @@ mod parse_single_amount_tests {
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(
result.unwrap(), result.unwrap(),
Amount { (
operator: Operator::Plus, Amount {
element: Element::Variable("abc".to_string()) operator: Operator::Plus,
} element: Element::Variable("abc".to_string())
},
""
)
) )
} }
@ -233,24 +229,15 @@ mod parse_single_amount_tests {
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(
result.unwrap(), result.unwrap(),
Amount { (
operator: Operator::Plus, Amount {
element: Element::Number(1) 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());
}
} }
#[cfg(test)] #[cfg(test)]
@ -263,20 +250,26 @@ mod parse_many_amounts_tests {
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(
result.unwrap(), result.unwrap(),
vec![Amount { (
operator: Operator::Plus, vec![Amount {
element: Element::Number(1) operator: Operator::Plus,
}] element: Element::Number(1)
}],
""
)
); );
let result = parse_amounts("10"); let result = parse_amounts("10");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(
result.unwrap(), result.unwrap(),
vec![Amount { (
operator: Operator::Plus, vec![Amount {
element: Element::Number(10) operator: Operator::Plus,
}] element: Element::Number(10)
}],
""
)
); );
} }
@ -295,20 +288,26 @@ mod parse_many_amounts_tests {
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(
result.unwrap(), result.unwrap(),
vec![Amount { (
operator: Operator::Plus, vec![Amount {
element: Element::Variable("asdf".to_string()) operator: Operator::Plus,
}] element: Element::Variable("asdf".to_string())
}],
""
)
); );
let result = parse_amounts("nosis"); let result = parse_amounts("nosis");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(
result.unwrap(), result.unwrap(),
vec![Amount { (
operator: Operator::Plus, vec![Amount {
element: Element::Variable("nosis".to_string()) 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 crate::{config::Config, db::sqlite::Database};
use futures::stream; use futures::stream;
use futures::{StreamExt, TryFutureExt, TryStreamExt}; use futures::{StreamExt, TryFutureExt, TryStreamExt};
use matrix_sdk::{identifiers::UserId, room::Joined, Client}; use matrix_sdk::ruma::OwnedUserId;
use matrix_sdk::{room::Joined, Client};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::sync::Arc; use std::sync::Arc;
use tenebrous_rpc::protos::dicebot::{ use tenebrous_rpc::protos::dicebot::{
@ -84,7 +85,7 @@ impl Dicebot for DicebotRpcService {
request: Request<UserIdRequest>, request: Request<UserIdRequest>,
) -> Result<Response<RoomsListReply>, Status> { ) -> Result<Response<RoomsListReply>, Status> {
let UserIdRequest { user_id } = request.into_inner(); let UserIdRequest { user_id } = request.into_inner();
let user_id = UserId::try_from(user_id).map_err(BotError::from)?; let user_id = OwnedUserId::try_from(user_id).map_err(BotError::from)?;
let rooms_for_user = matrix::get_rooms_for_user(&self.client, &user_id) let rooms_for_user = matrix::get_rooms_for_user(&self.client, &user_id)
.err_into::<BotError>() .err_into::<BotError>()
@ -94,7 +95,7 @@ impl Dicebot for DicebotRpcService {
.filter_map(|room: Joined| async move { .filter_map(|room: Joined| async move {
let room: Result<Room, _> = room.display_name().await.map(|room_name| Room { let room: Result<Room, _> = room.display_name().await.map(|room_name| Room {
room_id: room.room_id().to_string(), room_id: room.room_id().to_string(),
display_name: room_name, display_name: room_name.to_string(),
}); });
Some(room) Some(room)

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,6 +3,10 @@ name = "tenebrous-rpc"
version = "0.1.0" version = "0.1.0"
authors = ["projectmoon <projectmoon@agnos.is>"] authors = ["projectmoon <projectmoon@agnos.is>"]
edition = "2018" edition = "2018"
description = "gRPC protobuf models for Tenebrous."
homepage = "https://git.agnos.is/projectmoon/tenebrous-dicebot"
repository = "https://git.agnos.is/projectmoon/tenebrous-dicebot"
license = "AGPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

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