Compare commits

..

10 Commits

11 changed files with 1229 additions and 992 deletions

2
.envrc Normal file
View File

@ -0,0 +1,2 @@
use flake

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/target
/.idea
/.env
/.direnv

1887
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,11 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
matrix-sdk = { version = "0.4.1", features = [ "encryption", "sled_cryptostore" ] }
#monzo-lib = "0.4.0"
monzo-lib = { git = "https://github.com/danieleades/monzo-lib.git", rev = "e54ff827" }
# we use new features not quite yet in a released matrix rust SDK
matrix-sdk = "0.5.0"
#matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev = "a49a7fe1f93a4e8791dbbe4fd6876c587c33d2fa", features = ["encryption", "sled_cryptostore"] }
monzo-lib = "0.4.4"
#monzo-lib = { git = "https://github.com/danieleades/monzo-lib.git", rev = "e54ff827" }
tokio = { version = "1.13.0", features = [ "full" ] }
anyhow = "1.0.45"
warp = "0.3.1"

81
flake.lock Normal file
View File

@ -0,0 +1,81 @@
{
"nodes": {
"naersk": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1698420672,
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
"owner": "nix-community",
"repo": "naersk",
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1714971268,
"narHash": "sha256-IKwMSwHj9+ec660l+I4tki/1NRoeGpyA2GdtdYpAgEw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "27c13997bf450a01219899f5a83bd6ffbfc70d3c",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-23.11",
"type": "indirect"
}
},
"root": {
"inputs": {
"naersk": "naersk",
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

47
flake.nix Normal file
View File

@ -0,0 +1,47 @@
{
description = "Matrix Monzo bot";
inputs = {
nixpkgs.url = "nixpkgs/nixos-23.11";
utils.url = "github:numtide/flake-utils";
naersk = {
url = "github:nix-community/naersk";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, utils, naersk }:
utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages."${system}";
naersk-lib = naersk.lib."${system}";
in rec {
# `nix build`
packages.mxmonzo = naersk-lib.buildPackage {
pname = "mxmonzo";
root = ./.;
buildInputs = with pkgs; [
openssl
pkg-config
];
};
defaultPackage = packages.mxmonzo;
# NixOS Modules
nixosModules = {
mxmonzo = import ./nixos_modules/mxmonzo.nix self;
};
# `nix run`
apps.mxmonzo = utils.lib.mkApp {
drv = packages.mxmonzo;
};
defaultApp = apps.mxmonzo;
# `nix develop`
devShell = pkgs.mkShell {
nativeBuildInputs = with pkgs; [ rustc cargo openssl pkg-config ];
};
});
}

114
nixos_modules/mxmonzo.nix Normal file
View File

@ -0,0 +1,114 @@
flake: {config, pkgs, lib, ...}:
let
cfg = config.services.mxmonzo;
inherit (flake.packages.${pkgs.stdenv.hostPlatform.system}) mxmonzo;
in
with lib;
{
options = {
services.mxmonzo = {
enable = mkOption {
default = false;
type = with types; bool;
description = ''
Start the Matrix Monzo bot.
'';
};
user = mkOption {
default = "mxmonzo";
type = with types; uniq str;
description = ''
Name of the user.
'';
};
matrixId = mkOption {
type = with types; str;
example = "@monzobot:librepush.net";
description = ''
Matrix ID of the Monzo bot user.
'';
};
bindAddress = mkOption {
default = "127.0.0.1:38320";
type = with types; str;
description = ''
Host:Port upon which to bind the web interface (used for OAuth + webhooks).
'';
};
externalBaseUri = mkOption {
example = "https://mxmonzo.my.librepush.net";
type = with types; str;
description = ''
External URL prefix to which this MxMonzo instance can be accessed (by webhooks).
It should be proxied to the HTTP interface listening on `bindAddress`.
'';
};
environmentFile = mkOption {
type = with types; path;
description = ''
File containing environment variables, especially:
- MATRIX_PASSWORD
- MONZO_CLIENT_ID
- MONZO_CLIENT_SECRET
'';
};
matrixRoom = mkOption {
type = with types; str;
description = ''
Room ID, like !roomid:librepush.net, of the bot's room to answer commands and emit notifications to.
'';
};
dataPath = mkOption {
type = with types; path;
description = ''
Path to where data can be kept.
'';
};
};
};
config = mkIf cfg.enable {
users.users."${cfg.user}" = {
description = "Matrix Monzo User";
isSystemUser = true;
group = "${cfg.user}";
};
users.groups."${cfg.user}" = {};
systemd.services.mxmonzo = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
description = "Start the Matrix Monzo bot.";
environment = {
BIND_ADDRESS = cfg.bindAddress;
MATRIX_ID = cfg.matrixId;
MATRIX_ROOM = cfg.matrixRoom;
MATRIX_STORE = "${cfg.dataPath}/matrix-sdk";
MATRIX_PERSIST = "${cfg.dataPath}/matrix.json";
MONZO_PERSIST = "${cfg.dataPath}/monzo.json";
BASE_URI = cfg.externalBaseUri;
};
serviceConfig = {
Type = "simple";
User = "${cfg.user}";
ExecStart = ''${mxmonzo}/bin/mxmonzo'';
EnvironmentFile = [
cfg.environmentFile
];
};
};
};
}

View File

@ -1,12 +1,15 @@
use crate::monzo::monzo_client_freshened;
use crate::state::{Config, MonzoState, State, StateInner};
use crate::web::warp_main;
use matrix_sdk::config::SyncSettings;
use matrix_sdk::room::Room;
use matrix_sdk::ruma::events::room::message::{MessageEventContent, MessageType};
use matrix_sdk::ruma::events::SyncMessageEvent;
use matrix_sdk::ruma::events::room::message::{MessageType, RoomMessageEventContent};
use matrix_sdk::ruma::events::OriginalSyncMessageLikeEvent;
use matrix_sdk::ruma::UserId;
use matrix_sdk::{Client, ClientConfig, SyncSettings};
use matrix_sdk::store::StateStore;
use matrix_sdk::{Client, Session};
use std::convert::TryFrom;
use std::fs::File;
use std::sync::Arc;
use tokio::io::AsyncReadExt;
use tokio::sync::RwLock;
@ -16,7 +19,7 @@ pub mod state;
pub mod web;
async fn on_room_message(
event: &SyncMessageEvent<MessageEventContent>,
event: &OriginalSyncMessageLikeEvent<RoomMessageEventContent>,
room: Room,
state: &State,
) -> anyhow::Result<()> {
@ -45,10 +48,10 @@ async fn on_room_message(
buf.push_str("?currency");
}
}
let content = MessageEventContent::text_plain(&buf);
let content = RoomMessageEventContent::text_plain(&buf);
room.send(content, None).await.unwrap();
} else {
let content = MessageEventContent::text_plain("Not linked :(.");
let content = RoomMessageEventContent::text_plain("Not linked :(.");
room.send(content, None).await.unwrap();
}
}
@ -64,12 +67,22 @@ async fn main() -> anyhow::Result<()> {
let state_inner = StateInner::default();
let mxid = UserId::try_from(config.matrix_id.clone())?;
let mxid = <&UserId>::try_from(config.matrix_id.as_str())?;
let store_path = config.matrix_store.clone();
let client_config = ClientConfig::new().store_path(store_path);
let state_store = Box::new(StateStore::open_with_path(store_path.join("state"))?);
let client = Client::new_from_user_id_with_config(mxid.clone(), client_config).await?;
let crypto_store = Box::new(matrix_sdk::store::CryptoStore::open_with_passphrase(
store_path.join("crypto"),
None,
)?);
let client = Client::builder()
.crypto_store(crypto_store)
.state_store(state_store)
.user_id(&mxid)
.build()
.await?;
let state = State {
config: Arc::new(config.clone()),
@ -77,15 +90,24 @@ async fn main() -> anyhow::Result<()> {
matrix_client: Arc::new(client.clone()),
};
if config.matrix_persist.exists() {
eprintln!("Restoring Matrix session...");
let session: Session = serde_json::from_reader(File::open(config.matrix_persist)?)?;
client.restore_login(session).await?;
} else {
eprintln!("Logging in!");
client
let session: Session = client
.login(
mxid.localpart(),
&config.matrix_password,
Some("mxmonzo"),
None,
Some("rei's MxMonzo"),
)
.await?;
.await?
.into();
eprintln!("Persisting Matrix session...");
serde_json::to_writer(File::create(config.matrix_persist)?, &session)?;
}
eprintln!("Syncing once!");
client.sync_once(SyncSettings::new()).await?;
@ -111,7 +133,7 @@ async fn main() -> anyhow::Result<()> {
client
.register_event_handler(
move |ev: SyncMessageEvent<MessageEventContent>, room: Room| {
move |ev: OriginalSyncMessageLikeEvent<RoomMessageEventContent>, room: Room| {
let state_arc = state_arc.clone();
async move {
if let Err(error) = on_room_message(&ev, room, &state_arc).await {

View File

@ -73,7 +73,11 @@ pub async fn monzo_client_freshened(
drop(state_inner);
let needs_refresh = exp_at_instant < SystemTime::now();
if needs_refresh {
eprintln!("Refreshing token; expiry {:?}; now {:?}.", exp_at_instant, SystemTime::now());
eprintln!(
"Refreshing token; expiry {:?}; now {:?}.",
exp_at_instant,
SystemTime::now()
);
eprintln!("DEBUG refresh_token being used {:?}", &refresh_token);
let client = reqwest::Client::new();
let resp = client
@ -87,8 +91,7 @@ pub async fn monzo_client_freshened(
.send()
.await?;
eprintln!("DEBUG response status {:?}", resp.status());
let jv: serde_json::Value = resp.json()
.await?;
let jv: serde_json::Value = resp.json().await?;
eprintln!("DEBUG raw JV {:#?}", jv);
let auth_resp: MonzoAuthResponse = serde_json::from_value(jv)?;
let mut state_inner = state.inner.write().await;

View File

@ -19,6 +19,7 @@ pub struct Config {
pub matrix_room: String,
pub matrix_store: PathBuf,
pub matrix_password: String,
pub matrix_persist: PathBuf,
pub monzo_persist: PathBuf,
pub monzo_client_id: String,
pub monzo_client_secret: String,

View File

@ -3,7 +3,7 @@ use crate::monzo::{
};
use crate::state::State;
use matrix_sdk::room::Room;
use matrix_sdk::ruma::events::room::message::MessageEventContent;
use matrix_sdk::ruma::events::room::message::RoomMessageEventContent;
use matrix_sdk::ruma::RoomId;
use monzo::accounts::Type;
use serde::Deserialize;
@ -55,7 +55,7 @@ async fn auth_done(query: AuthDone, state: State) -> anyhow::Result<impl warp::R
let _client = monzo_client_from_code(&state, &query.code).await?;
Ok("Success! Please authorise in Monzo, then click <a href='/auth_confirmed'>here</a>.")
Ok(warp::reply::html("Success! Please authorise in Monzo, then click <a href='/auth_confirmed'>here</a>."))
}
async fn auth_done_wrapped(
@ -140,7 +140,7 @@ async fn monzo_hook(hook: MonzoHook, state: State) -> anyhow::Result<impl warp::
let room = state
.matrix_client
.get_room(&RoomId::from_str(&state.config.matrix_room)?)
.get_room(<&RoomId>::try_from(state.config.matrix_room.as_str())?)
.ok_or_else(|| anyhow::anyhow!("Matrix room not found!"))?;
if let Room::Joined(room) = room {
let mut buf = String::new();
@ -177,6 +177,8 @@ async fn monzo_hook(hook: MonzoHook, state: State) -> anyhow::Result<impl warp::
.unwrap_or("unk");
buf.push_str(&format!(" on {}", on_account_name));
// MUST drop state_inner to avoid deadlock
drop(state_inner);
if let Some(monzo_client) = monzo_client_freshened(&state).await? {
// include new balance
match monzo_client.balance(&txn_created.account_id).await {
@ -191,10 +193,9 @@ async fn monzo_hook(hook: MonzoHook, state: State) -> anyhow::Result<impl warp::
eprintln!("when querying balance after webhook: {:?}", err);
}
}
}
let content = MessageEventContent::text_plain(&buf);
let content = RoomMessageEventContent::text_plain(&buf);
room.send(content, None).await.unwrap();
} else {
eprintln!("Not in room.");