Compare commits

...

18 Commits

Author SHA1 Message Date
Olivier 'reivilibre' 2afdfc0445 Update flake and make nixpkgs a top-level input 2024-05-08 11:44:21 +01:00
Olivier 'reivilibre' f7f363aa9e Remove `src` input indirection that invalidates flake for no good reason 2023-10-30 20:01:16 +00:00
Olivier 'reivilibre' f902f2a45a Reply with HTML content type 2023-10-28 21:57:22 +01:00
Olivier 'reivilibre' 8bb5e037bc Fixup flake for development 2023-10-28 21:54:12 +01:00
Olivier 'reivilibre' 7c142e8080 Add Nix flake with NixOS module
ci/woodpecker/push/woodpecker Pipeline is pending Details
2022-05-23 22:52:35 +01:00
Olivier 'reivilibre' 3f2170263b Upgrade to matrix-rust-sdk 0.5.0 2022-05-23 21:52:07 +01:00
Olivier 'reivilibre' e8fcd33a01 Fix guaranteed deadlock when refreshing token during webhook handling
continuous-integration/drone the build was successful Details
2021-11-26 20:11:28 +00:00
Olivier 'reivilibre' eef55632bb Add some minor debug prints
continuous-integration/drone the build was successful Details
2021-11-23 22:30:46 +00:00
Olivier 'reivilibre' d560b860bf Login in a more restorable way 2021-11-23 22:24:49 +00:00
Olivier 'reivilibre' 808d45b893 Update to Matrix SDK from git (breaking, probably 0.5 prerelease) 2021-11-23 22:24:27 +00:00
Olivier 'reivilibre' f77ccefd7c Add some debug logging
continuous-integration/drone the build was successful Details
2021-11-13 08:00:26 +00:00
Olivier 'reivilibre' dbf2e24369 Make webhook handler more robust to API failures 2021-11-13 07:57:08 +00:00
Olivier 'reivilibre' 926519c908 Add logging when we refresh a token to see if it ever works 2021-11-13 07:56:36 +00:00
Olivier 'reivilibre' e34d84d7fb Fix old use of env var
continuous-integration/drone the build was successful Details
2021-11-08 20:01:52 +00:00
Olivier 'reivilibre' 10fc9e4acd CI fixups and simplifications
continuous-integration/drone the build was successful Details
2021-11-08 13:14:16 +00:00
Olivier 'reivilibre' 6185572b7c experiment
continuous-integration/drone the build was successful Details
2021-11-08 13:12:05 +00:00
Olivier 'reivilibre' dab4666b3e experiment
continuous-integration/drone the build was successful Details
2021-11-08 13:09:57 +00:00
Olivier 'reivilibre' 9dfca7ec16 Experiment 2021-11-08 13:09:36 +00:00
12 changed files with 1253 additions and 1006 deletions

2
.envrc Normal file
View File

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

1
.gitignore vendored
View File

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

View File

@ -6,11 +6,10 @@ pipeline:
event: [push, pull_request] event: [push, pull_request]
image: rust:1.56.1-slim image: rust:1.56.1-slim
commands: commands:
- echo nop - apt-get -qq update && apt-get -yqq install pkg-config libssl-dev build-essential libolm-dev cmake
#- apt-get -qq update && apt-get -yqq install pkg-config libssl-dev build-essential libolm-dev cmake
#- rustup component add clippy #- rustup component add clippy
# TODO clippy one day # TODO clippy one day
#- cargo check - cargo check
buildRelease: buildRelease:
when: when:
@ -18,14 +17,11 @@ pipeline:
tag: v* tag: v*
image: rust:1.56.1-slim image: rust:1.56.1-slim
commands: commands:
#- apt-get -qq update && apt-get -yqq install pkg-config libssl-dev build-essential libolm-dev cmake - apt-get -qq update && apt-get -yqq install pkg-config libssl-dev build-essential libolm-dev cmake
#- cargo build --release - cargo build --release
- "dir=$DRONE_REPO_NAME-$DRONE_TAG-$${DRONE_ARCH/'/'/-}" - "qualname=`echo $DRONE_REPO_NAME-$DRONE_TAG-${DRONE_ARCH/\\//-} | sed s%/%-%`"
- "mkdir $dir"
- cp target/release/mxmonzo $dir/
- tar cvf $dir.tar.gz $dir
- mkdir dist - mkdir dist
- mv $dir.tar.gz dist/ - cp target/release/mxmonzo dist/$qualname
uploadRelease: uploadRelease:
when: when:

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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
matrix-sdk = { version = "0.4.1", features = [ "encryption", "sled_cryptostore" ] } # we use new features not quite yet in a released matrix rust SDK
#monzo-lib = "0.4.0" matrix-sdk = "0.5.0"
monzo-lib = { git = "https://github.com/danieleades/monzo-lib.git", rev = "e54ff827" } #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" ] } tokio = { version = "1.13.0", features = [ "full" ] }
anyhow = "1.0.45" anyhow = "1.0.45"
warp = "0.3.1" 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::monzo::monzo_client_freshened;
use crate::state::{Config, MonzoState, State, StateInner}; use crate::state::{Config, MonzoState, State, StateInner};
use crate::web::warp_main; use crate::web::warp_main;
use matrix_sdk::config::SyncSettings;
use matrix_sdk::room::Room; use matrix_sdk::room::Room;
use matrix_sdk::ruma::events::room::message::{MessageEventContent, MessageType}; use matrix_sdk::ruma::events::room::message::{MessageType, RoomMessageEventContent};
use matrix_sdk::ruma::events::SyncMessageEvent; use matrix_sdk::ruma::events::OriginalSyncMessageLikeEvent;
use matrix_sdk::ruma::UserId; 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::convert::TryFrom;
use std::fs::File;
use std::sync::Arc; use std::sync::Arc;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -16,7 +19,7 @@ pub mod state;
pub mod web; pub mod web;
async fn on_room_message( async fn on_room_message(
event: &SyncMessageEvent<MessageEventContent>, event: &OriginalSyncMessageLikeEvent<RoomMessageEventContent>,
room: Room, room: Room,
state: &State, state: &State,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -45,10 +48,10 @@ async fn on_room_message(
buf.push_str("?currency"); buf.push_str("?currency");
} }
} }
let content = MessageEventContent::text_plain(&buf); let content = RoomMessageEventContent::text_plain(&buf);
room.send(content, None).await.unwrap(); room.send(content, None).await.unwrap();
} else { } else {
let content = MessageEventContent::text_plain("Not linked :(."); let content = RoomMessageEventContent::text_plain("Not linked :(.");
room.send(content, None).await.unwrap(); room.send(content, None).await.unwrap();
} }
} }
@ -64,12 +67,22 @@ async fn main() -> anyhow::Result<()> {
let state_inner = StateInner::default(); 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 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 { let state = State {
config: Arc::new(config.clone()), config: Arc::new(config.clone()),
@ -77,15 +90,24 @@ async fn main() -> anyhow::Result<()> {
matrix_client: Arc::new(client.clone()), matrix_client: Arc::new(client.clone()),
}; };
eprintln!("Logging in!"); if config.matrix_persist.exists() {
client eprintln!("Restoring Matrix session...");
.login( let session: Session = serde_json::from_reader(File::open(config.matrix_persist)?)?;
mxid.localpart(), client.restore_login(session).await?;
&config.matrix_password, } else {
Some("mxmonzo"), eprintln!("Logging in!");
Some("rei's MxMonzo"), let session: Session = client
) .login(
.await?; mxid.localpart(),
&config.matrix_password,
None,
Some("rei's MxMonzo"),
)
.await?
.into();
eprintln!("Persisting Matrix session...");
serde_json::to_writer(File::create(config.matrix_persist)?, &session)?;
}
eprintln!("Syncing once!"); eprintln!("Syncing once!");
client.sync_once(SyncSettings::new()).await?; client.sync_once(SyncSettings::new()).await?;
@ -111,7 +133,7 @@ async fn main() -> anyhow::Result<()> {
client client
.register_event_handler( .register_event_handler(
move |ev: SyncMessageEvent<MessageEventContent>, room: Room| { move |ev: OriginalSyncMessageLikeEvent<RoomMessageEventContent>, room: Room| {
let state_arc = state_arc.clone(); let state_arc = state_arc.clone();
async move { async move {
if let Err(error) = on_room_message(&ev, room, &state_arc).await { if let Err(error) = on_room_message(&ev, room, &state_arc).await {

View File

@ -73,8 +73,14 @@ pub async fn monzo_client_freshened(
drop(state_inner); drop(state_inner);
let needs_refresh = exp_at_instant < SystemTime::now(); let needs_refresh = exp_at_instant < SystemTime::now();
if needs_refresh { if needs_refresh {
eprintln!(
"Refreshing token; expiry {:?}; now {:?}.",
exp_at_instant,
SystemTime::now()
);
eprintln!("DEBUG refresh_token being used {:?}", &refresh_token);
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let auth_resp: MonzoAuthResponse = client let resp = client
.post("https://api.monzo.com/oauth2/token") .post("https://api.monzo.com/oauth2/token")
.form(&[ .form(&[
("grant_type", "refresh_token"), ("grant_type", "refresh_token"),
@ -83,9 +89,11 @@ pub async fn monzo_client_freshened(
("refresh_token", &refresh_token), ("refresh_token", &refresh_token),
]) ])
.send() .send()
.await?
.json()
.await?; .await?;
eprintln!("DEBUG response status {:?}", resp.status());
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; let mut state_inner = state.inner.write().await;
let monzo_state = state_inner.monzo_state.as_mut().unwrap(); let monzo_state = state_inner.monzo_state.as_mut().unwrap();
monzo_state.access_token = auth_resp.access_token; monzo_state.access_token = auth_resp.access_token;

View File

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

View File

@ -3,7 +3,7 @@ use crate::monzo::{
}; };
use crate::state::State; use crate::state::State;
use matrix_sdk::room::Room; 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 matrix_sdk::ruma::RoomId;
use monzo::accounts::Type; use monzo::accounts::Type;
use serde::Deserialize; use serde::Deserialize;
@ -33,7 +33,7 @@ async fn auth_setup(
/* host: String, forwarded_proto: Option<String>, */ state: State, /* host: String, forwarded_proto: Option<String>, */ state: State,
) -> Result<impl warp::Reply, warp::Rejection> { ) -> Result<impl warp::Reply, warp::Rejection> {
// let final_redirect_uri = format!("{}://{}/auth_done", forwarded_proto.as_deref().unwrap_or("http"), host); // let final_redirect_uri = format!("{}://{}/auth_done", forwarded_proto.as_deref().unwrap_or("http"), host);
let final_redirect_uri = std::env::var("MONZO_REDIRECT_URI").unwrap(); let final_redirect_uri = format!("{}/auth_done", state.config.base_uri);
let mut url = Url::parse("https://auth.monzo.com/").unwrap(); let mut url = Url::parse("https://auth.monzo.com/").unwrap();
url.query_pairs_mut() url.query_pairs_mut()
.append_pair("client_id", &state.config.monzo_client_id) .append_pair("client_id", &state.config.monzo_client_id)
@ -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?; 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( async fn auth_done_wrapped(
@ -140,7 +140,7 @@ async fn monzo_hook(hook: MonzoHook, state: State) -> anyhow::Result<impl warp::
let room = state let room = state
.matrix_client .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!"))?; .ok_or_else(|| anyhow::anyhow!("Matrix room not found!"))?;
if let Room::Joined(room) = room { if let Room::Joined(room) = room {
let mut buf = String::new(); let mut buf = String::new();
@ -177,15 +177,25 @@ async fn monzo_hook(hook: MonzoHook, state: State) -> anyhow::Result<impl warp::
.unwrap_or("unk"); .unwrap_or("unk");
buf.push_str(&format!(" on {}", on_account_name)); 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? { if let Some(monzo_client) = monzo_client_freshened(&state).await? {
// include new balance // include new balance
let new_bal = monzo_client.balance(&txn_created.account_id).await?.balance; match monzo_client.balance(&txn_created.account_id).await {
let now_quid = new_bal / 100; Ok(resp) => {
let now_pennies = new_bal % 100; let new_bal = resp.balance;
buf.push_str(&format!(" now £{}.{:02}", now_quid, now_pennies)); let now_quid = new_bal / 100;
let now_pennies = new_bal % 100;
buf.push_str(&format!(" now £{}.{:02}", now_quid, now_pennies));
}
Err(err) => {
buf.push_str(". Error when getting now.");
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(); room.send(content, None).await.unwrap();
} else { } else {
eprintln!("Not in room."); eprintln!("Not in room.");