Compare commits
18 Commits
|
@ -1,3 +1,4 @@
|
|||
/target
|
||||
/.idea
|
||||
/.env
|
||||
/.direnv
|
||||
|
|
|
@ -6,11 +6,10 @@ pipeline:
|
|||
event: [push, pull_request]
|
||||
image: rust:1.56.1-slim
|
||||
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
|
||||
# TODO clippy one day
|
||||
#- cargo check
|
||||
- cargo check
|
||||
|
||||
buildRelease:
|
||||
when:
|
||||
|
@ -18,14 +17,11 @@ pipeline:
|
|||
tag: v*
|
||||
image: rust:1.56.1-slim
|
||||
commands:
|
||||
#- apt-get -qq update && apt-get -yqq install pkg-config libssl-dev build-essential libolm-dev cmake
|
||||
#- cargo build --release
|
||||
- "dir=$DRONE_REPO_NAME-$DRONE_TAG-$${DRONE_ARCH/'/'/-}"
|
||||
- "mkdir $dir"
|
||||
- cp target/release/mxmonzo $dir/
|
||||
- tar cvf $dir.tar.gz $dir
|
||||
- apt-get -qq update && apt-get -yqq install pkg-config libssl-dev build-essential libolm-dev cmake
|
||||
- cargo build --release
|
||||
- "qualname=`echo $DRONE_REPO_NAME-$DRONE_TAG-${DRONE_ARCH/\\//-} | sed s%/%-%`"
|
||||
- mkdir dist
|
||||
- mv $dir.tar.gz dist/
|
||||
- cp target/release/mxmonzo dist/$qualname
|
||||
|
||||
uploadRelease:
|
||||
when:
|
||||
|
|
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
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
60
src/main.rs
60
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()),
|
||||
};
|
||||
|
||||
eprintln!("Logging in!");
|
||||
client
|
||||
.login(
|
||||
mxid.localpart(),
|
||||
&config.matrix_password,
|
||||
Some("mxmonzo"),
|
||||
Some("rei's MxMonzo"),
|
||||
)
|
||||
.await?;
|
||||
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!");
|
||||
let session: Session = client
|
||||
.login(
|
||||
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!");
|
||||
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 {
|
||||
|
|
14
src/monzo.rs
14
src/monzo.rs
|
@ -73,8 +73,14 @@ 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!("DEBUG refresh_token being used {:?}", &refresh_token);
|
||||
let client = reqwest::Client::new();
|
||||
let auth_resp: MonzoAuthResponse = client
|
||||
let resp = client
|
||||
.post("https://api.monzo.com/oauth2/token")
|
||||
.form(&[
|
||||
("grant_type", "refresh_token"),
|
||||
|
@ -83,9 +89,11 @@ pub async fn monzo_client_freshened(
|
|||
("refresh_token", &refresh_token),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.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 monzo_state = state_inner.monzo_state.as_mut().unwrap();
|
||||
monzo_state.access_token = auth_resp.access_token;
|
||||
|
|
|
@ -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,
|
||||
|
|
28
src/web.rs
28
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;
|
||||
|
@ -33,7 +33,7 @@ async fn auth_setup(
|
|||
/* host: String, forwarded_proto: Option<String>, */ state: State,
|
||||
) -> 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 = 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();
|
||||
url.query_pairs_mut()
|
||||
.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?;
|
||||
|
||||
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,15 +177,25 @@ 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
|
||||
let new_bal = monzo_client.balance(&txn_created.account_id).await?.balance;
|
||||
let now_quid = new_bal / 100;
|
||||
let now_pennies = new_bal % 100;
|
||||
buf.push_str(&format!(" now £{}.{:02}", now_quid, now_pennies));
|
||||
match monzo_client.balance(&txn_created.account_id).await {
|
||||
Ok(resp) => {
|
||||
let new_bal = resp.balance;
|
||||
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();
|
||||
} else {
|
||||
eprintln!("Not in room.");
|
||||
|
|
Loading…
Reference in New Issue