Compare commits
10 Commits
Author | SHA1 | Date |
---|---|---|
Olivier 'reivilibre' | 2afdfc0445 | |
Olivier 'reivilibre' | f7f363aa9e | |
Olivier 'reivilibre' | f902f2a45a | |
Olivier 'reivilibre' | 8bb5e037bc | |
Olivier 'reivilibre' | 7c142e8080 | |
Olivier 'reivilibre' | 3f2170263b | |
Olivier 'reivilibre' | e8fcd33a01 | |
Olivier 'reivilibre' | eef55632bb | |
Olivier 'reivilibre' | d560b860bf | |
Olivier 'reivilibre' | 808d45b893 |
|
@ -1,3 +1,4 @@
|
|||
/target
|
||||
/.idea
|
||||
/.env
|
||||
/.direnv
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 ];
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
48
src/main.rs
48
src/main.rs
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
11
src/web.rs
11
src/web.rs
|
@ -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.");
|
||||
|
|
Loading…
Reference in New Issue