first release
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
LordMZTE 2021-06-18 23:15:19 +02:00
parent 5e4ff37992
commit 87fc6a9f0b
6 changed files with 408 additions and 46 deletions

View file

@ -11,12 +11,22 @@ anyhow = "1.0"
clap = "2.33.3"
env_logger = "0.8.4"
log = "0.4.13"
mime = "0.3.16"
mime_guess = "2.0.3"
rand = "0.8.4"
serde_json = "1.0.61"
sled = "0.34.6"
structopt = "0.3.21"
toml = "0.5.8"
[dependencies.jm_client_core]
#path = "/home/lordmzte/dev/jensmemesclient/jensmemesclient/jm_client_core"
git = "https://tilera.xyz/git/lordmzte/jensmemesclient.git"
package = "jm_client_core"
rev = "0d3a77"
[dependencies.matrix-sdk]
version = "0.2.0"
git = "https://github.com/matrix-org/matrix-rust-sdk.git"
features = ["encryption"]
[dependencies.url]
@ -28,5 +38,5 @@ version = "1.0"
features = ["derive"]
[dependencies.tokio]
version = "0.2"
version = "1.7.0"
features = ["macros"]

View file

@ -1,7 +1,56 @@
# URL of the homeserver to use
homeserver_url = "https://matrix.org"
user_id = "@rufftest:matrix.org"
password = "xxx"
device_name = "RUFF"
# path to store databases
store_path = "store"
# MEMES!!
memes = [
# random stuff
{ keyword = "alec", id = 650 },
{ keyword = "bastard", id = 375 },
{ keyword = "drogen", id = 191 },
{ keyword = "fresse", id = 375 },
{ keyword = "hendrik", randomcat = "hendrik" },
{ keyword = "hey", id = 243 },
{ keyword = "itbyhf", id = 314 },
{ keyword = "jonasled", id = 164 },
{ keyword = "kappa", id = 182 },
{ keyword = "lordmzte", id = 315 },
{ keyword = "realtox", id = 168 },
{ keyword = "sklave", id = 304 },
{ keyword = "tilera", id = 316 },
# üffen
{ keyword = "uff", randomcat = "uff" },
{ keyword = "biguff", id = 771 },
{ keyword = "hmm", id = 892 },
{ keyword = "hmmm", id = 891 },
{ keyword = "longuff", id = 771 },
{ keyword = "uffal", id = 654 },
{ keyword = "uffat", id = 257 },
{ keyword = "uffch", id = 286 },
{ keyword = "uffde", id = 144 },
{ keyword = "uffgo", id = 568 },
{ keyword = "uffhf", id = 645 },
{ keyword = "uffhre", id = 312 },
{ keyword = "uffhs", id = 331 },
{ keyword = "uffj", id = 626 },
{ keyword = "uffjl", id = 773 },
{ keyword = "uffjs", id = 615 },
{ keyword = "uffkt", id = 627 },
{ keyword = "ufflie", id = 284 },
{ keyword = "uffmj", id = 831 },
{ keyword = "uffmz", id = 646 },
{ keyword = "uffns", id = 287 },
{ keyword = "uffpy", id = 477 },
{ keyword = "uffrs", id = 616 },
{ keyword = "uffrt", id = 986 },
{ keyword = "uffru", id = 999 },
{ keyword = "uffsb", id = 818 },
{ keyword = "uffsr", id = 585 },
{ keyword = "ufftl", id = 644 },
{ keyword = "uffwe", id = 779 },
]
[memes]
uff = 144

View file

@ -1,9 +1,8 @@
use matrix_sdk::identifiers::UserId;
use serde::{
de::{self, Deserializer, Visitor},
Deserialize,
};
use std::{collections::BTreeMap, convert::TryFrom};
use crate::meme::Meme;
use crate::meme::MemeIdent;
use serde::de::{self, Deserializer, MapAccess, Visitor};
use serde::Deserialize;
use std::path::PathBuf;
use url::Url;
#[derive(Debug, Deserialize)]
@ -12,5 +11,113 @@ pub struct Config {
pub user_id: String,
pub password: String,
pub device_name: Option<String>,
pub memes: BTreeMap<String, u32>,
pub memes: Vec<Meme>,
pub store_path: PathBuf,
#[serde(default = "default_clear_threshold")]
pub clear_cache_threshold: u32,
}
fn default_clear_threshold() -> u32 {
10
}
impl<'de> Deserialize<'de> for Meme {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
const FIELDS: &[&str] = &["keyword", "id", "randomcat"];
enum Field {
Keyword,
Ident(IdentField),
}
enum IdentField {
RandomCat,
Id,
}
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct Vis;
impl<'de> Visitor<'de> for Vis {
type Value = Field;
fn expecting(
&self,
fmt: &mut std::fmt::Formatter<'_>,
) -> Result<(), std::fmt::Error> {
fmt.write_str("a field for a meme")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(match v {
"keyword" => Field::Keyword,
"randomcat" => Field::Ident(IdentField::RandomCat),
"id" => Field::Ident(IdentField::Id),
_ => return Err(de::Error::unknown_field(v, FIELDS)),
})
}
}
deserializer.deserialize_identifier(Vis)
}
}
struct Vis;
impl<'de> Visitor<'de> for Vis {
type Value = Meme;
fn expecting(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
fmt.write_str("a meme")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut keyword = None;
let mut ident = None;
while let Some(key) = map.next_key()? {
match key {
Field::Keyword => {
if keyword.is_some() {
return Err(de::Error::duplicate_field("keyword"));
}
keyword = Some(map.next_value()?);
}
Field::Ident(i) => {
if ident.is_some() {
return Err(de::Error::duplicate_field(
"ident, can only have one.",
));
}
match i {
IdentField::Id => {
ident = Some(MemeIdent::Id(map.next_value()?));
}
IdentField::RandomCat => {
ident = Some(MemeIdent::RandomCat(map.next_value()?));
}
}
}
}
}
let keyword = keyword.ok_or_else(|| de::Error::missing_field("keyword"))?;
let ident = ident.ok_or_else(|| de::Error::missing_field("ident"))?;
Ok(Meme { keyword, ident })
}
}
deserializer.deserialize_struct("Meme", FIELDS, Vis)
}
}

View file

@ -1,25 +1,34 @@
use anyhow::{anyhow, bail, Context};
use jm_client_core::JMClient;
use log::{error, info, warn};
use matrix_sdk::SyncRoom;
use matrix_sdk::{
api::r0::{session::login, sync::sync_events},
api::r0::session::login,
async_trait,
deserialized_responses::SyncResponse,
events::{
room::{
member::MemberEventContent,
message::{MessageEventContent, TextMessageEventContent},
message::{MessageEventContent, MessageType, TextMessageEventContent},
},
AnyMessageEventContent, AnyToDeviceEvent, StrippedStateEvent, SyncMessageEvent,
AnyToDeviceEvent, StrippedStateEvent, SyncMessageEvent,
},
EventEmitter, LoopCtrl,
room::Room,
verification::Verification,
EventHandler, LoopCtrl,
};
use rand::{rngs::StdRng, SeedableRng};
use sled::Db;
use std::{
collections::BTreeMap,
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
sync::{
atomic::{AtomicBool, AtomicU32},
Arc,
},
time::Duration,
};
use structopt::StructOpt;
use tokio::sync::Mutex;
use tokio::sync::RwLock;
use config::Config;
@ -27,6 +36,8 @@ use config::Config;
use matrix_sdk::{self, api::r0::uiaa::AuthData, identifiers::UserId, Client, SyncSettings};
use serde_json::json;
mod config;
mod meme;
mod responder;
#[derive(Debug, StructOpt)]
struct Opt {
@ -47,6 +58,7 @@ async fn main() -> anyhow::Result<()> {
let config = std::fs::read(&opt.config).map_err(|e| anyhow!("Error reading config: {}", e))?;
let config =
toml::from_slice::<Config>(&config).map_err(|e| anyhow!("Error parsing config: {}", e))?;
let config = Arc::new(config);
let client = Arc::new(RwLock::new(Client::new(config.homeserver_url.clone())?));
@ -60,8 +72,14 @@ async fn main() -> anyhow::Result<()> {
client
.write()
.await
.add_event_emitter(Box::new(Bot {
.set_event_handler(Box::new(Bot {
client: Arc::clone(&client),
jm_client: RwLock::new(JMClient::new()),
memecache: sled::open(config.store_path.join("memecache"))
.map_err(|e| anyhow!("error opening memecache: {}", e))?,
config: Arc::clone(&config),
meme_count: AtomicU32::new(0),
rng: Mutex::new(StdRng::from_rng(rand::thread_rng())?),
}))
.await;
@ -82,8 +100,7 @@ async fn main() -> anyhow::Result<()> {
if initial.load(std::sync::atomic::Ordering::SeqCst) {
if let Err(e) =
on_initial_response(&response, client_ref, &user_id_ref, &config_ref.password)
.await
on_initial_response(client_ref, &user_id_ref, &config_ref.password).await
{
error!("Error processing initial response: {}", e);
}
@ -98,15 +115,23 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}
struct Bot {
pub struct Bot {
client: Arc<RwLock<Client>>,
jm_client: RwLock<JMClient>,
memecache: Db,
config: Arc<Config>,
/// used to keep track of how many memes have been sent.
/// this is reset once the threshold set in the config has been reached, and the JMClient cache
/// is cleared.
meme_count: AtomicU32,
rng: Mutex<StdRng>,
}
#[async_trait]
impl EventEmitter for Bot {
impl EventHandler for Bot {
async fn on_stripped_state_member(
&self,
room: SyncRoom,
room: Room,
room_member: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
@ -114,16 +139,15 @@ impl EventEmitter for Bot {
return;
}
if let SyncRoom::Invited(room) = room {
let room = room.read().await;
println!("Autojoining room {}", room.room_id);
if let Room::Invited(room) = room {
println!("Autojoining room {}", room.room_id());
let mut delay = 2;
while let Err(err) = self
.client
.read()
.await
.join_room_by_id(&room.room_id)
.join_room_by_id(&room.room_id())
.await
{
// retry autojoin due to synapse sending invites, before the
@ -131,44 +155,64 @@ impl EventEmitter for Bot {
// https://github.com/matrix-org/synapse/issues/4345
warn!(
"Failed to join room {} ({:?}), retrying in {}s",
room.room_id, err, delay
room.room_id(),
err,
delay
);
tokio::time::delay_for(Duration::from_secs(delay)).await;
tokio::time::sleep(Duration::from_secs(delay)).await;
delay *= 2;
if delay > 3600 {
error!("Can't join room {} ({:?})", room.room_id, err);
error!("Can't join room {} ({:?})", room.room_id(), err);
break;
}
}
info!("Successfully joined room {}", room.room_id);
info!("Successfully joined room {}", room.room_id());
}
}
async fn on_room_message(&self, _room: SyncRoom, msg: &SyncMessageEvent<MessageEventContent>) {
dbg!(msg);
async fn on_room_message(&self, room: Room, msg: &SyncMessageEvent<MessageEventContent>) {
if self
.client
.read()
.await
.user_id()
.await
.map(|u| u == msg.sender)
.unwrap_or(true)
{
return;
}
if let SyncMessageEvent {
content:
MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
..
},
..
} = msg
{
if let Err(e) = responder::on_msg(msg_body, room, self).await {
error!("Responder error: {}", e);
}
}
}
}
async fn on_initial_response(
_response: &sync_events::Response,
client: &Client,
user_id: &UserId,
password: &str,
) -> anyhow::Result<()> {
bootstrap_cross_signing(client, user_id, password).await?;
for (id, _room) in client.joined_rooms().read().await.iter() {
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::Text(
TextMessageEventContent::plain("Hello world"),
));
client.room_send(id, content, None).await?;
}
Ok(())
}
async fn on_response(response: &sync_events::Response, client: &Client) -> anyhow::Result<()> {
async fn on_response(response: &SyncResponse, client: &Client) -> anyhow::Result<()> {
for event in response
.to_device
.events
@ -178,7 +222,10 @@ async fn on_response(response: &sync_events::Response, client: &Client) -> anyho
match event {
AnyToDeviceEvent::KeyVerificationStart(e) => {
info!("Starting verification");
if let Some(sas) = &client.get_verification(&e.content.transaction_id).await {
if let Some(Verification::SasV1(sas)) = &client
.get_verification(&e.sender, &e.content.transaction_id)
.await
{
if let Err(e) = sas.accept().await {
error!("Error accepting key verification request: {}", e);
}
@ -186,7 +233,10 @@ async fn on_response(response: &sync_events::Response, client: &Client) -> anyho
}
AnyToDeviceEvent::KeyVerificationKey(e) => {
if let Some(sas) = &client.get_verification(&e.content.transaction_id).await {
if let Some(Verification::SasV1(sas)) = &client
.get_verification(&e.sender, &e.content.transaction_id)
.await
{
if let Err(e) = sas.confirm().await {
error!("Error confirming key verification request: {}", e);
}
@ -223,7 +273,7 @@ async fn bootstrap_cross_signing(
password: &str,
) -> anyhow::Result<()> {
info!("bootstrapping e2e");
if let Err(e) = dbg!(client.bootstrap_cross_signing(None).await) {
if let Err(e) = client.bootstrap_cross_signing(None).await {
warn!("couldnt bootstrap e2e without auth data");
if let Some(response) = e.uiaa_response() {
let auth_data = auth_data(&user_id, &password, response.session.as_deref());
@ -231,7 +281,7 @@ async fn bootstrap_cross_signing(
.bootstrap_cross_signing(Some(auth_data))
.await
.context("Couldn't bootstrap cross signing")?;
info!("bootstrapped e2e with auth data");
info!("bootstrapped e2e with auth data");
} else {
bail!("Error during cross-signing bootstrap {:#?}", e);
}

39
src/meme.rs Normal file
View file

@ -0,0 +1,39 @@
use crate::Bot;
use rand::seq::IteratorRandom;
#[derive(Debug)]
pub struct Meme {
pub keyword: String,
pub ident: MemeIdent,
}
impl Meme {
/// checks if the meme should be triggered for the given message
pub fn matches(&self, msg: &str) -> bool {
let msg = msg.to_ascii_lowercase();
msg.starts_with(&self.keyword) &&
// msg must have one of allowed chars after keyword
msg.chars().nth(self.keyword.len()).map(|c|" ,.;:!?({-_".contains(c)).unwrap_or(true)
}
pub async fn get_meme(&self, bot: &Bot) -> anyhow::Result<Option<jm_client_core::api::Meme>> {
let memes = bot.jm_client.read().await.get_memes().await?;
match &self.ident {
MemeIdent::Id(i) => Ok(memes
.iter()
.find(|m| m.id.parse::<u32>().ok() == Some(*i))
.cloned()),
MemeIdent::RandomCat(c) => Ok(memes
.iter()
.filter(|m| &m.category == c)
.choose(&mut *bot.rng.lock().await)
.cloned()),
}
}
}
#[derive(Debug)]
pub enum MemeIdent {
RandomCat(String),
Id(u32),
}

107
src/responder.rs Normal file
View file

@ -0,0 +1,107 @@
use crate::Bot;
use anyhow::Context;
use log::{error, info, warn};
use matrix_sdk::{
api::r0::media::create_content,
events::{
room::message::{ImageMessageEventContent, MessageEventContent, MessageType},
AnyMessageEventContent,
},
identifiers::MxcUri,
room::{Joined, Room},
};
use std::io::Cursor;
use std::sync::atomic::Ordering;
pub async fn on_msg(msg: &str, room: Room, bot: &Bot) -> anyhow::Result<()> {
let room = match room {
Room::Joined(room) => room,
_ => {
warn!(
"Received message '{}' in room {:?} that's not joined",
msg,
room.name()
);
return Ok(());
}
};
for meme in &bot.config.memes {
if meme.matches(msg) {
bot.meme_count.fetch_add(1, Ordering::SeqCst);
if bot.meme_count.load(Ordering::SeqCst) >= bot.config.clear_cache_threshold {
bot.jm_client.write().await.clear_cache().await;
bot.meme_count.store(0, Ordering::SeqCst);
}
let meme_name = &meme.keyword;
if let Some(meme) = meme.get_meme(bot).await? {
match meme.id.parse::<u32>() {
Err(e) => {
error!("Meme {:?} has invalid ID! tilera, you messed up with your stupid php again: {}", &meme, e);
}
Ok(id) => {
if let Some(ivec) = bot.memecache.get(id.to_le_bytes())? {
info!("Meme {} found in cache!", id);
let mxc = String::from_utf8(ivec.as_ref().to_vec()).context(
"Found invalid utf8 mxc url in memecache! Is the cache borked?",
)?;
send_meme(&room, mxc.into(), meme_name.clone()).await?;
} else {
info!("Meme {} not found in cache, uploading...", id);
let resp = bot
.jm_client
.read()
.await
.http
.get(&meme.link)
.send()
.await
.context("error downloading meme")?;
let resp = resp.bytes().await?;
if let Some(mime) = mime_guess::from_path(&meme.link).first() {
let create_content::Response { content_uri, .. } = bot
.client
.read()
.await
.upload(&mime, &mut Cursor::new(resp))
.await?;
bot.memecache.insert(
id.to_le_bytes(),
content_uri.to_string().into_bytes(),
)?;
send_meme(&room, content_uri, meme_name.clone()).await?;
} else {
error!(
"Couldn't guess MIME type of meme '{}', skipping.",
&meme.link
);
}
}
}
}
} else {
error!("Found meme with invalid id! {:?}", &meme);
}
}
}
Ok(())
}
async fn send_meme(room: &Joined, mxc: MxcUri, meme_name: String) -> anyhow::Result<()> {
room.send(
AnyMessageEventContent::RoomMessage(MessageEventContent::new(MessageType::Image(
ImageMessageEventContent::plain(meme_name, mxc, None),
))),
None,
)
.await
.context("Failed to send meme")?;
Ok(())
}