Compare commits
1
Commits
main
..
ad118fbec9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad118fbec9 |
Generated
+798
-1206
File diff suppressed because it is too large
Load Diff
+1
-3
@@ -14,7 +14,6 @@ resvg = "*"
|
|||||||
oauth1 = "*"
|
oauth1 = "*"
|
||||||
clap = { version = "*", features = ["derive"] }
|
clap = { version = "*", features = ["derive"] }
|
||||||
webbrowser = "*"
|
webbrowser = "*"
|
||||||
#webbrowser = { path = "../webbrowser-rs" }
|
|
||||||
itertools = "*"
|
itertools = "*"
|
||||||
tracing = "0.1.36"
|
tracing = "0.1.36"
|
||||||
tracing-subscriber = "0.3.15"
|
tracing-subscriber = "0.3.15"
|
||||||
@@ -22,10 +21,9 @@ regex = "1.6.0"
|
|||||||
image = "0.24.3"
|
image = "0.24.3"
|
||||||
viuer = "0.6.1"
|
viuer = "0.6.1"
|
||||||
url = { version = "2.3.1", features = ["serde"] }
|
url = { version = "2.3.1", features = ["serde"] }
|
||||||
megalodon = "*"
|
megalodon = { git = "https://github.com/wlcx/megalodon-rs.git" }
|
||||||
tokio = "*"
|
tokio = "*"
|
||||||
futures-util = "*"
|
futures-util = "*"
|
||||||
bsky-sdk = "0.1.13"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
toml = "*"
|
toml = "*"
|
||||||
|
|||||||
Generated
+107
-58
@@ -2,16 +2,15 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"devshell": {
|
"devshell": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": "nixpkgs",
|
||||||
"nixpkgs"
|
"systems": "systems"
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768818222,
|
"lastModified": 1686680692,
|
||||||
"narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
|
"narHash": "sha256-SsLZz3TDleraAiJq4EkmdyewSyiv5g0LZYc6vaLZOMQ=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "devshell",
|
"repo": "devshell",
|
||||||
"rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
|
"rev": "fd6223370774dd9c33354e87a007004b5fd36442",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -20,41 +19,34 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fenix": {
|
"flake-utils": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"systems": "systems_2"
|
||||||
"naersk",
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"rust-analyzer-src": "rust-analyzer-src"
|
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1752475459,
|
"lastModified": 1681202837,
|
||||||
"narHash": "sha256-z6QEu4ZFuHiqdOPbYss4/Q8B0BFhacR8ts6jO/F/aOU=",
|
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||||
"owner": "nix-community",
|
"owner": "numtide",
|
||||||
"repo": "fenix",
|
"repo": "flake-utils",
|
||||||
"rev": "bf0d6f70f4c9a9cf8845f992105652173f4b617f",
|
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nix-community",
|
"owner": "numtide",
|
||||||
"repo": "fenix",
|
"repo": "flake-utils",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"naersk": {
|
"naersk": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"fenix": "fenix",
|
"nixpkgs": "nixpkgs_2"
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1782220280,
|
"lastModified": 1686572087,
|
||||||
"narHash": "sha256-thLTFbp9D5Qknmh8q/v4FRpLGphUSijT3E86cbLYTXo=",
|
"narHash": "sha256-jXTut7ZSYqLEgm/nTk7TuVL2ExahTip605bLINklAnQ=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "naersk",
|
"repo": "naersk",
|
||||||
"rev": "9aa07bb0256d300219b30622d2454e85f7f3667e",
|
"rev": "8507af04eb40c5520bd35d9ce6f9d2342cea5ad1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -65,10 +57,26 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1764831616,
|
"lastModified": 1677383253,
|
||||||
"narHash": "sha256-OtzF5wBvO0jgW1WW1rQU9cMGx7zuvkF7CAVJ1ypzkxA=",
|
"narHash": "sha256-UfpzWfSxkfXHnb4boXZNaKsAcUrZT9Hw+tao1oZxd08=",
|
||||||
"path": "/nix/store/hv1dx0bah4m1mw2asfrg29wl17by9ngd-source",
|
"owner": "NixOS",
|
||||||
"rev": "c97c47f2bac4fa59e2cbdeba289686ae615f8ed4",
|
"repo": "nixpkgs",
|
||||||
|
"rev": "9952d6bc395f5841262b006fbace8dd7e143b634",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1685789966,
|
||||||
|
"narHash": "sha256-pyqctu5Cq1jwymO3Os0/RNj5Nm3q5kmRCT24p7gtG70=",
|
||||||
|
"path": "/nix/store/hnkjxwx9zv2k0gkiznbpkrsvyrzaz6w1-source",
|
||||||
|
"rev": "4eaa9e3eb36386de0c6a268ba5da72cafc959619",
|
||||||
"type": "path"
|
"type": "path"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -76,44 +84,55 @@
|
|||||||
"type": "indirect"
|
"type": "indirect"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nixpkgs_3": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1685789966,
|
||||||
|
"narHash": "sha256-pyqctu5Cq1jwymO3Os0/RNj5Nm3q5kmRCT24p7gtG70=",
|
||||||
|
"path": "/nix/store/hnkjxwx9zv2k0gkiznbpkrsvyrzaz6w1-source",
|
||||||
|
"rev": "4eaa9e3eb36386de0c6a268ba5da72cafc959619",
|
||||||
|
"type": "path"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_4": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681358109,
|
||||||
|
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"devshell": "devshell",
|
"devshell": "devshell",
|
||||||
"naersk": "naersk",
|
"naersk": "naersk",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs_3",
|
||||||
"rust-overlay": "rust-overlay",
|
"rust-overlay": "rust-overlay",
|
||||||
"utils": "utils"
|
"utils": "utils"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rust-analyzer-src": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1752428706,
|
|
||||||
"narHash": "sha256-EJcdxw3aXfP8Ex1Nm3s0awyH9egQvB2Gu+QEnJn2Sfg=",
|
|
||||||
"owner": "rust-lang",
|
|
||||||
"repo": "rust-analyzer",
|
|
||||||
"rev": "591e3b7624be97e4443ea7b5542c191311aa141d",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "rust-lang",
|
|
||||||
"ref": "nightly",
|
|
||||||
"repo": "rust-analyzer",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-overlay": {
|
"rust-overlay": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs"
|
"nixpkgs": "nixpkgs_4"
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1787834454,
|
"lastModified": 1686795910,
|
||||||
"narHash": "sha256-jXSvqn04IOecJCvSvl8+g3PsbDVNP6qKs6dKUs/b16k=",
|
"narHash": "sha256-jDa40qRZ0GRQtP9EMZdf+uCbvzuLnJglTUI2JoHfWDc=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "dc2fd1acc537f3583744e1373597a5731ff7a6e3",
|
"rev": "5c2b97c0a9bc5217fc3dfb1555aae0fb756d99f9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -137,16 +156,46 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"systems_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems_3": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"utils": {
|
"utils": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"systems": "systems"
|
"systems": "systems_3"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1731533236,
|
"lastModified": 1685518550,
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -3,18 +3,9 @@
|
|||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
utils.url = "github:numtide/flake-utils";
|
utils.url = "github:numtide/flake-utils";
|
||||||
devshell = {
|
devshell.url = "github:numtide/devshell";
|
||||||
url = "github:numtide/devshell";
|
naersk.url = "github:nix-community/naersk";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
};
|
|
||||||
naersk = {
|
|
||||||
url = "github:nix-community/naersk";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
rust-overlay = {
|
|
||||||
url = "github:oxalica/rust-overlay";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs = {
|
||||||
@@ -41,7 +32,7 @@
|
|||||||
packig = naersk-lib.buildPackage {
|
packig = naersk-lib.buildPackage {
|
||||||
pname = "iso7010-a-day";
|
pname = "iso7010-a-day";
|
||||||
root = ./.;
|
root = ./.;
|
||||||
buildInputs = [pkgs.openssl pkgs.pkg-config];
|
buildInputs = [pkgs.openssl pkgs.pkgconfig];
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
packages.default = packig;
|
packages.default = packig;
|
||||||
@@ -61,45 +52,5 @@
|
|||||||
packages = with pkgs; [rust rust-analyzer];
|
packages = with pkgs; [rust rust-analyzer];
|
||||||
};
|
};
|
||||||
formatter = pkgs.alejandra;
|
formatter = pkgs.alejandra;
|
||||||
}) // {
|
});
|
||||||
overlays.default = final: prev: {
|
|
||||||
iso7010-a-day = self.packages.${prev.system}.default;
|
|
||||||
};
|
|
||||||
|
|
||||||
nixosModules.default = {
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
with lib; let
|
|
||||||
cfg = config.services.iso7010-a-day;
|
|
||||||
in {
|
|
||||||
options.services.iso7010-a-day = {
|
|
||||||
enable = mkEnableOption "Enable the iso7010 bot";
|
|
||||||
environmentFile = mkOption {
|
|
||||||
type = types.path;
|
|
||||||
description = "Path to the environment file";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
config = mkIf cfg.enable {
|
|
||||||
nixpkgs.overlays = [self.overlays.default];
|
|
||||||
systemd.services.iso7010-a-day = {
|
|
||||||
description = "Samw's ISO7010 bot";
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
DynamicUser = true;
|
|
||||||
User = "iso7010";
|
|
||||||
ExecStart = "${pkgs.iso7010-a-day}/bin/iso7010_a_day run-bot";
|
|
||||||
EnvironmentFile = cfg.environmentFile;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
systemd.timers.iso7010-a-day = {
|
|
||||||
wantedBy = [ "timers.target" ];
|
|
||||||
partOf = [ "iso7010-a-day.service" ];
|
|
||||||
timerConfig.OnCalendar = [ "*-*-* 12:00:00" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 9.1 KiB |
-70
@@ -1,70 +0,0 @@
|
|||||||
use std::{
|
|
||||||
borrow::Cow,
|
|
||||||
io::{Cursor, Seek, SeekFrom},
|
|
||||||
};
|
|
||||||
|
|
||||||
use bsky_sdk::{
|
|
||||||
api::{
|
|
||||||
app::bsky::{
|
|
||||||
embed::images::{ImageData, MainData},
|
|
||||||
feed::post::{RecordData, RecordEmbedRefs},
|
|
||||||
},
|
|
||||||
types::string::Datetime,
|
|
||||||
},
|
|
||||||
BskyAgent,
|
|
||||||
};
|
|
||||||
|
|
||||||
const BSKY_IDENT_ENV_VAR: &str = "BSKY_IDENTIFIER";
|
|
||||||
const BSKY_PASSWORD_ENV_VAR: &str = "BSKY_PASSWORD";
|
|
||||||
|
|
||||||
fn getenv(name: &str) -> String {
|
|
||||||
std::env::var(name).expect(&format!("Please specify {} in the env", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn post(
|
|
||||||
text: &str,
|
|
||||||
img: Option<Box<Cursor<Vec<u8>>>>,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let agent = BskyAgent::builder().build().await?;
|
|
||||||
agent
|
|
||||||
.login(getenv(BSKY_IDENT_ENV_VAR), getenv(BSKY_PASSWORD_ENV_VAR))
|
|
||||||
.await?;
|
|
||||||
let embed = if let Some(mut i) = img {
|
|
||||||
i.as_mut().seek(SeekFrom::Start(0))?;
|
|
||||||
let out = agent
|
|
||||||
.api
|
|
||||||
.com
|
|
||||||
.atproto
|
|
||||||
.repo
|
|
||||||
.upload_blob(i.into_inner())
|
|
||||||
.await?;
|
|
||||||
let embed = RecordEmbedRefs::AppBskyEmbedImagesMain(Box::new(
|
|
||||||
MainData {
|
|
||||||
images: vec![ImageData {
|
|
||||||
alt: "".into(),
|
|
||||||
aspect_ratio: None,
|
|
||||||
image: out.data.blob,
|
|
||||||
}
|
|
||||||
.into()],
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
));
|
|
||||||
Some(bsky_sdk::api::types::Union::Refs(embed))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
agent
|
|
||||||
.create_record(RecordData {
|
|
||||||
created_at: Datetime::now(),
|
|
||||||
embed,
|
|
||||||
entities: None,
|
|
||||||
facets: None,
|
|
||||||
labels: None,
|
|
||||||
langs: None,
|
|
||||||
reply: None,
|
|
||||||
tags: None,
|
|
||||||
text: text.into(),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
+66
-29
@@ -1,16 +1,17 @@
|
|||||||
use mastodon::authorize_fedi;
|
use mastodon::authorize_fedi;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::fs::File;
|
use std::io::Cursor;
|
||||||
use std::io::{BufWriter, Cursor, Write};
|
|
||||||
use tracing::{event, Level};
|
use tracing::{event, Level};
|
||||||
mod bsky;
|
|
||||||
mod mastodon;
|
mod mastodon;
|
||||||
mod svg;
|
|
||||||
mod twitter;
|
mod twitter;
|
||||||
mod wiki;
|
mod wiki;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
use image::{DynamicImage, RgbaImage};
|
||||||
|
use resvg::tiny_skia::{Paint, PathBuilder, Pixmap, PixmapPaint, Stroke};
|
||||||
|
use resvg::usvg::TreeParsing;
|
||||||
|
use resvg::usvg::{Options, Transform};
|
||||||
|
use resvg::Tree;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use twitter::*;
|
use twitter::*;
|
||||||
use wiki::*;
|
use wiki::*;
|
||||||
@@ -26,6 +27,63 @@ static APP_USER_AGENT: &str = concat!(
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Render the raw SVG data to an image
|
||||||
|
fn render_svg(data: &[u8], height: u32, with_border: bool) -> StdError<DynamicImage> {
|
||||||
|
let opt = Options::default();
|
||||||
|
let rtree = resvg::usvg::Tree::from_data(data, &opt).expect("couldn't parse");
|
||||||
|
let svg_size = rtree.size;
|
||||||
|
// Work out how wide the pixmap of height `height` needs to be to entirely fit the SVG.
|
||||||
|
let scale_factor = height as f32 / svg_size.height();
|
||||||
|
let pm_width = (scale_factor * svg_size.width()).ceil() as u32;
|
||||||
|
let mut pixmap = Pixmap::new(pm_width, height).ok_or("Error creating pixmap")?;
|
||||||
|
// Render the svg into a pixmap.
|
||||||
|
Tree::from_usvg(&rtree).render(
|
||||||
|
Transform::from_scale(scale_factor, scale_factor),
|
||||||
|
&mut pixmap.as_mut(),
|
||||||
|
);
|
||||||
|
// Make a wider pixmap with a 16:9 AR and the same height. This is a blesséd ratio by twitter
|
||||||
|
// and means we see the whole image nicely in the timeline with no truncation.
|
||||||
|
let mut bigger_pixmap =
|
||||||
|
Pixmap::new(height / 9 * 16, height).ok_or("Error creating bigger pixmap")?;
|
||||||
|
// Then draw our freshly rendered SVG into the middle of the bigger pixmap.
|
||||||
|
bigger_pixmap.draw_pixmap(
|
||||||
|
((bigger_pixmap.width() - pm_width) / 2).try_into().unwrap(),
|
||||||
|
0,
|
||||||
|
pixmap.as_ref(),
|
||||||
|
&PixmapPaint::default(),
|
||||||
|
Transform::identity(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let (w, h) = (bigger_pixmap.width(), bigger_pixmap.height());
|
||||||
|
// Render a red border for debug purposes
|
||||||
|
if with_border {
|
||||||
|
let mut paint = Paint::default();
|
||||||
|
paint.set_color_rgba8(255, 0, 0, 255);
|
||||||
|
let stroke = Stroke {
|
||||||
|
width: 1.0,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = {
|
||||||
|
let mut pb = PathBuilder::new();
|
||||||
|
pb.move_to(0.0, 0.0);
|
||||||
|
pb.line_to(0.0, h as f32 - stroke.width);
|
||||||
|
pb.line_to(w as f32, h as f32 - stroke.width);
|
||||||
|
pb.line_to(w as f32 - stroke.width, 0.0);
|
||||||
|
pb.line_to(0.0, 0.0);
|
||||||
|
pb.finish().unwrap()
|
||||||
|
};
|
||||||
|
bigger_pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
|
||||||
|
}
|
||||||
|
let img = RgbaImage::from_raw(
|
||||||
|
bigger_pixmap.width(),
|
||||||
|
bigger_pixmap.height(),
|
||||||
|
bigger_pixmap.data().to_vec(),
|
||||||
|
)
|
||||||
|
.ok_or("Error creating image from pixmap")?;
|
||||||
|
Ok(DynamicImage::ImageRgba8(img))
|
||||||
|
}
|
||||||
|
|
||||||
type StdError<T> = Result<T, Box<dyn std::error::Error>>;
|
type StdError<T> = Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@@ -56,8 +114,6 @@ enum Commands {
|
|||||||
},
|
},
|
||||||
/// Print details about the currently authed user
|
/// Print details about the currently authed user
|
||||||
Whoami,
|
Whoami,
|
||||||
/// Download and dump all images to disk
|
|
||||||
Dump,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -72,7 +128,6 @@ async fn main() -> StdError<()> {
|
|||||||
Commands::Whoami => do_whoami().await,
|
Commands::Whoami => do_whoami().await,
|
||||||
Commands::AuthorizeFedi => authorize_fedi().await,
|
Commands::AuthorizeFedi => authorize_fedi().await,
|
||||||
Commands::RunBot { dry_run, target } => run_bot(*dry_run, target.to_owned()).await,
|
Commands::RunBot { dry_run, target } => run_bot(*dry_run, target.to_owned()).await,
|
||||||
Commands::Dump => do_dump().await,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,22 +237,6 @@ fn get_client(headers: Option<reqwest::header::HeaderMap>) -> StdError<reqwest::
|
|||||||
Ok(c.build()?)
|
Ok(c.build()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn do_dump() -> StdError<()> {
|
|
||||||
let all = scrape_web().await?;
|
|
||||||
let filenames: Vec<_> = all.iter().map(|(_, filename)| filename.as_str()).collect();
|
|
||||||
let metas = get_file_metadata(&filenames).await?;
|
|
||||||
// for m in &metas {
|
|
||||||
// let bytes = client.get(m.url.to_string()).await?.bytes().await?;
|
|
||||||
// }
|
|
||||||
let file = File::create("dump.json")?;
|
|
||||||
let mut writer = BufWriter::new(file);
|
|
||||||
serde_json::to_writer_pretty(&mut writer, &metas)?;
|
|
||||||
writer.flush()?;
|
|
||||||
let count = metas.len();
|
|
||||||
event!(Level::INFO, count, "Dumped records to json file");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_bot(dry_run: bool, target: Option<String>) -> StdError<()> {
|
async fn run_bot(dry_run: bool, target: Option<String>) -> StdError<()> {
|
||||||
let all = scrape_web().await?;
|
let all = scrape_web().await?;
|
||||||
let (title, filename) = if let Some(target) = target {
|
let (title, filename) = if let Some(target) = target {
|
||||||
@@ -228,15 +267,13 @@ async fn run_bot(dry_run: bool, target: Option<String>) -> StdError<()> {
|
|||||||
|
|
||||||
if !dry_run {
|
if !dry_run {
|
||||||
// Render the image nice and big for twitter
|
// Render the image nice and big for twitter
|
||||||
let img = svg::render_svg(&svg, 1000, false)?;
|
let img = render_svg(&svg, 1000, false)?;
|
||||||
let mut buf = Cursor::new(Vec::new());
|
let mut buf = Cursor::new(Vec::new());
|
||||||
img.write_to(&mut buf, image::ImageFormat::Png)?;
|
img.write_to(&mut buf, image::ImageFormat::Png)?;
|
||||||
let the_img = Some(Box::new(buf));
|
toot(&text, Some(buf.into_inner().into())).await?;
|
||||||
toot(&text, the_img.clone()).await?;
|
|
||||||
bsky::post(&text, the_img).await?;
|
|
||||||
} else {
|
} else {
|
||||||
// Render the image smaller for output to terminal
|
// Render the image smaller for output to terminal
|
||||||
let img = svg::render_svg(&svg, 128, true)?;
|
let img = render_svg(&svg, 128, true)?;
|
||||||
println!("Dry run - would tweet:\n \"{}\"", text);
|
println!("Dry run - would tweet:\n \"{}\"", text);
|
||||||
viuer::print(
|
viuer::print(
|
||||||
&img,
|
&img,
|
||||||
|
|||||||
+10
-6
@@ -1,17 +1,18 @@
|
|||||||
// Interface to mastodon (etc) instances
|
// Interface to mastodon (etc) instances
|
||||||
|
|
||||||
use megalodon::entities::UploadMedia;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use megalodon::entities::{Attachment, UploadMedia};
|
||||||
use megalodon::generator;
|
use megalodon::generator;
|
||||||
use megalodon::megalodon::PostStatusInputOptions;
|
use megalodon::megalodon::PostStatusInputOptions;
|
||||||
use megalodon::{self, megalodon::UploadMediaInputOptions};
|
use megalodon::{self, megalodon::UploadMediaInputOptions};
|
||||||
use std::io::{Cursor, Seek, SeekFrom};
|
|
||||||
|
|
||||||
use crate::{StdError, APP_USER_AGENT};
|
use crate::{StdError, APP_USER_AGENT};
|
||||||
|
|
||||||
const FEDI_ACCESS_TOKEN_ENV_VAR: &str = "FEDI_ACCESS_TOKEN";
|
const FEDI_ACCESS_TOKEN_ENV_VAR: &str = "FEDI_ACCESS_TOKEN";
|
||||||
const FEDI_INSTANCE_ENV_VAR: &str = "FEDI_INSTANCE";
|
const FEDI_INSTANCE_ENV_VAR: &str = "FEDI_INSTANCE";
|
||||||
|
|
||||||
pub async fn toot(text: &str, img: Option<Box<Cursor<Vec<u8>>>>) -> StdError<()> {
|
pub async fn toot(text: &str, img: Option<Cow<'static, [u8]>>) -> StdError<()> {
|
||||||
let client = megalodon::generator(
|
let client = megalodon::generator(
|
||||||
megalodon::SNS::Pleroma,
|
megalodon::SNS::Pleroma,
|
||||||
std::env::var(FEDI_INSTANCE_ENV_VAR)
|
std::env::var(FEDI_INSTANCE_ENV_VAR)
|
||||||
@@ -25,10 +26,13 @@ pub async fn toot(text: &str, img: Option<Box<Cursor<Vec<u8>>>>) -> StdError<()>
|
|||||||
Some(APP_USER_AGENT.into()),
|
Some(APP_USER_AGENT.into()),
|
||||||
);
|
);
|
||||||
let mut ops = PostStatusInputOptions::default();
|
let mut ops = PostStatusInputOptions::default();
|
||||||
if let Some(mut img) = img {
|
if let Some(img) = img {
|
||||||
img.as_mut().seek(SeekFrom::Start(0))?;
|
|
||||||
let media = client
|
let media = client
|
||||||
.upload_media_reader(img, Some(&UploadMediaInputOptions::default()))
|
.upload_media_raw(
|
||||||
|
// TODO: get better at lifetimes
|
||||||
|
Box::leak(Box::new(img)),
|
||||||
|
Some(&UploadMediaInputOptions::default()),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
ops.media_ids = Some(vec![match media.json {
|
ops.media_ids = Some(vec![match media.json {
|
||||||
UploadMedia::Attachment(a) => a.id,
|
UploadMedia::Attachment(a) => a.id,
|
||||||
|
|||||||
-64
@@ -1,64 +0,0 @@
|
|||||||
use image::{DynamicImage, RgbaImage};
|
|
||||||
use resvg::tiny_skia::{Paint, PathBuilder, Pixmap, PixmapPaint, Stroke};
|
|
||||||
use resvg::usvg::{fontdb, Options, PostProcessingSteps, Transform, TreeParsing, TreePostProc};
|
|
||||||
|
|
||||||
use crate::StdError;
|
|
||||||
|
|
||||||
// Render the raw SVG data to an image
|
|
||||||
pub fn render_svg(data: &[u8], height: u32, with_border: bool) -> StdError<DynamicImage> {
|
|
||||||
let opt = Options::default();
|
|
||||||
let mut rtree = resvg::usvg::Tree::from_data(data, &opt).expect("couldn't parse");
|
|
||||||
rtree.postprocess(PostProcessingSteps::default(), &fontdb::Database::new());
|
|
||||||
let svg_size = rtree.size;
|
|
||||||
// Work out how wide the pixmap of height `height` needs to be to entirely fit the SVG.
|
|
||||||
let scale_factor = height as f32 / svg_size.height();
|
|
||||||
let pm_width = (scale_factor * svg_size.width()).ceil() as u32;
|
|
||||||
let mut pixmap = Pixmap::new(pm_width, height).ok_or("Error creating pixmap")?;
|
|
||||||
// Render the svg into a pixmap.
|
|
||||||
resvg::render(
|
|
||||||
&rtree,
|
|
||||||
Transform::from_scale(scale_factor, scale_factor),
|
|
||||||
&mut pixmap.as_mut(),
|
|
||||||
);
|
|
||||||
// Make a wider pixmap with a 16:9 AR and the same height. This is a blesséd ratio by twitter
|
|
||||||
// and means we see the whole image nicely in the timeline with no truncation.
|
|
||||||
let mut bigger_pixmap =
|
|
||||||
Pixmap::new(height / 9 * 16, height).ok_or("Error creating bigger pixmap")?;
|
|
||||||
// Then draw our freshly rendered SVG into the middle of the bigger pixmap.
|
|
||||||
bigger_pixmap.draw_pixmap(
|
|
||||||
((bigger_pixmap.width() - pm_width) / 2).try_into().unwrap(),
|
|
||||||
0,
|
|
||||||
pixmap.as_ref(),
|
|
||||||
&PixmapPaint::default(),
|
|
||||||
Transform::identity(),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
let (w, h) = (bigger_pixmap.width(), bigger_pixmap.height());
|
|
||||||
// Render a red border for debug purposes
|
|
||||||
if with_border {
|
|
||||||
let mut paint = Paint::default();
|
|
||||||
paint.set_color_rgba8(255, 0, 0, 255);
|
|
||||||
let stroke = Stroke {
|
|
||||||
width: 1.0,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = {
|
|
||||||
let mut pb = PathBuilder::new();
|
|
||||||
pb.move_to(0.0, 0.0);
|
|
||||||
pb.line_to(0.0, h as f32 - stroke.width);
|
|
||||||
pb.line_to(w as f32, h as f32 - stroke.width);
|
|
||||||
pb.line_to(w as f32 - stroke.width, 0.0);
|
|
||||||
pb.line_to(0.0, 0.0);
|
|
||||||
pb.finish().unwrap()
|
|
||||||
};
|
|
||||||
bigger_pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
|
|
||||||
}
|
|
||||||
let img = RgbaImage::from_raw(
|
|
||||||
bigger_pixmap.width(),
|
|
||||||
bigger_pixmap.height(),
|
|
||||||
bigger_pixmap.data().to_vec(),
|
|
||||||
)
|
|
||||||
.ok_or("Error creating image from pixmap")?;
|
|
||||||
Ok(DynamicImage::ImageRgba8(img))
|
|
||||||
}
|
|
||||||
+4
-5
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use crate::{get_client, StdError};
|
use crate::{get_client, StdError};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use tracing::{event, instrument, Level};
|
use tracing::{event, instrument, Level};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@@ -19,9 +19,8 @@ pub async fn scrape_web() -> StdError<Vec<(String, String)>> {
|
|||||||
// Parse CSS selectors to scrape elements
|
// Parse CSS selectors to scrape elements
|
||||||
let gallerybox_sel =
|
let gallerybox_sel =
|
||||||
scraper::Selector::parse("li.gallerybox").map_err(|e| format!("{:?}", e))?;
|
scraper::Selector::parse("li.gallerybox").map_err(|e| format!("{:?}", e))?;
|
||||||
let link_sel =
|
let link_sel = scraper::Selector::parse("a.image").map_err(|e| format!("{:?}", e))?;
|
||||||
scraper::Selector::parse("a.mw-file-description").map_err(|e| format!("{:?}", e))?;
|
let title_sel = scraper::Selector::parse(".gallerytext p").map_err(|e| format!("{:?}", e))?;
|
||||||
let title_sel = scraper::Selector::parse(".gallerytext").map_err(|e| format!("{:?}", e))?;
|
|
||||||
|
|
||||||
// Fetch stuff!
|
// Fetch stuff!
|
||||||
let client = get_client(None)?;
|
let client = get_client(None)?;
|
||||||
@@ -95,7 +94,7 @@ pub async fn get_files_in_category(category: &str) -> StdError<Vec<String>> {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug)]
|
||||||
pub struct FileMeta {
|
pub struct FileMeta {
|
||||||
pub url: url::Url,
|
pub url: url::Url,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user