Compare commits

...

31 Commits

Author SHA1 Message Date
LordMZTE cda1743f0f add systemd unit file 2022-01-23 15:24:00 +01:00
LordMZTE c270953ab2 update libs 2022-01-23 15:16:51 +01:00
LordMZTE 653592649d update changelog
continuous-integration/drone/push Build is passing Details
2021-08-29 16:00:55 +02:00
LordMZTE 8e963c1852 fix uffch ID
continuous-integration/drone/push Build is passing Details
2021-08-29 15:59:59 +02:00
LordMZTE ae0bd48c38 delete obsolete dockerfile
continuous-integration/drone/push Build is passing Details
2021-08-17 23:03:14 +02:00
LordMZTE e72b92a678 update chagelog 2021-08-17 23:02:42 +02:00
LordMZTE c387ad566a update changelog 2021-08-17 23:01:38 +02:00
LordMZTE 54ce90d090 use upadted libjens instead of jm_client_core
continuous-integration/drone/push Build is passing Details
2021-08-17 23:00:37 +02:00
Timo Ley 70159a085d „CHANGELOG.md“ ändern
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-08-17 20:33:52 +00:00
Timo Ley 3c6ecd6649 update changelog
continuous-integration/drone/pr Build is passing Details
2021-08-17 20:31:20 +00:00
Timo Ley df15a1c3d5 Add npm meme 2021-08-17 20:30:51 +00:00
LordMZTE 28f610a1d2 add uffag and uff2d
continuous-integration/drone/push Build is failing Details
2021-08-06 15:41:29 +02:00
LordMZTE 9c1f1c44ac fix sendmeme command not sending new memes
continuous-integration/drone/push Build is failing Details
2021-07-31 20:45:44 +02:00
LordMZTE aea4b48c2b bump version 2021-07-31 20:44:02 +02:00
LordMZTE 5b115d4bbc new version
continuous-integration/drone/push Build is failing Details
2021-07-27 00:11:47 +02:00
LordMZTE 05e00f412b files that aren't videos or images are now uploaded as file
continuous-integration/drone/push Build is failing Details
2021-07-26 23:58:49 +02:00
LordMZTE 298c0a7f6f fmt 2021-07-26 23:58:04 +02:00
LordMZTE 5007f5ddba clean up drone config
continuous-integration/drone/push Build is failing Details
2021-07-17 22:59:16 +02:00
LordMZTE 485d44c56f add sendmeme command
continuous-integration/drone/push Build is failing Details
2021-07-17 15:43:01 +02:00
LordMZTE ccf8c65c80 memes have their file name as image description
continuous-integration/drone/push Build is failing Details
2021-07-17 14:47:03 +02:00
LordMZTE 568607fe3e Xd and xd commands + match_case parameter
continuous-integration/drone/push Build is failing Details
2021-07-10 21:40:37 +02:00
LordMZTE 58569bae3f fix contains matching & bump version
continuous-integration/drone/push Build is failing Details
2021-07-10 16:14:43 +02:00
LordMZTE 0e4664ceeb update changelog
continuous-integration/drone/push Build is failing Details
2021-07-08 20:56:51 +02:00
LordMZTE 0a3950af4f bump version and use fixed matrix-sdk version
continuous-integration/drone/push Build is failing Details
2021-07-08 20:31:57 +02:00
LordMZTE 8106dc3966 add wtf and wtuff commands and add "match" meme property
continuous-integration/drone/push Build is failing Details
2021-07-08 20:17:12 +02:00
LordMZTE 6322919e6d send mime type and size with memes
continuous-integration/drone/push Build is passing Details
2021-06-22 16:36:38 +02:00
LordMZTE cd1875240d reformat
continuous-integration/drone/push Build is passing Details
2021-06-22 14:16:47 +02:00
LordMZTE d0577cc66f fix clippy in CI
continuous-integration/drone/push Build is passing Details
2021-06-22 14:06:36 +02:00
LordMZTE 48d479973b meme cache now refreshed after response has been sent
continuous-integration/drone/push Build is failing Details
2021-06-22 14:01:33 +02:00
LordMZTE d8a846a8cb add uffso to exampleconfig
continuous-integration/drone/push Build is passing Details
2021-06-22 13:35:48 +02:00
LordMZTE a98c8cad14 itbyhf command now uses random itbyhf meme
continuous-integration/drone/push Build is passing Details
2021-06-19 21:48:48 +02:00
11 changed files with 572 additions and 335 deletions

View File

@ -1,36 +1,46 @@
kind: pipeline
type: docker
name: tests
steps:
- name: test-linux
image: rust
commands:
- apt update
- apt install -y cmake
- cargo test -v
- name: test-linux
image: rust
commands:
- apt update
- apt install -y cmake
- cargo test -v
- name: clippy-linux
image: rust
commands:
- apt update
- apt install -y cmake
- rustup component add clippy
- cargo clippy
---
kind: pipeline
type: docker
name: release
steps:
- name: release-linux
image: rust
commands:
- apt update
- apt install -y cmake
- cargo build --release -v
- strip target/release/ruff
- name: publish
image: plugins/gitea-release
settings:
base_url: https://tilera.xyz/git
api_key:
from_secret: gitea_token
note: CHANGELOG.md
title: tag-${DRONE_TAG}
files:
- target/release/ruff
when:
event: tag
depends_on:
- release-linux
- name: release-linux
image: rust
commands:
- apt update
- apt install -y cmake
- cargo build --release -v
- strip target/release/ruff
- name: publish
image: plugins/gitea-release
settings:
base_url: https://tilera.xyz/git
api_key:
from_secret: gitea_token
note: CHANGELOG.md
title: tag-${DRONE_TAG}
files:
- target/release/ruff
when:
event: [tag]
depends_on:
- release-linux

View File

@ -1,5 +1,2 @@
# 0.2.1
- added video support
- added `jens` command
- added `party` command
- meme cache now stores data encoded with bincode. **DELETE BEFORE UPDATE**
# 0.2.8
- fix uffch id

View File

@ -1,33 +1,32 @@
[package]
name = "ruff"
version = "0.2.1"
version = "0.2.8"
authors = ["LordMZTE <lord@mzte.de>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0"
anyhow = "1.0.53"
bincode = "1.3.3"
clap = "2.33.3"
env_logger = "0.8.4"
log = "0.4.13"
clap = "3.0.10"
env_logger = "0.9.0"
log = "0.4.14"
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"
serde_json = "1.0.78"
sled = "0.34.7"
structopt = "0.3.26"
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.libjens]
git = "https://tilera.xyz/git/lordmzte/libjens.git"
rev = "1.1.0"
[dependencies.matrix-sdk]
git = "https://github.com/matrix-org/matrix-rust-sdk.git"
rev = "c79e62d"
features = ["encryption"]
[dependencies.url]
@ -35,9 +34,9 @@ version = "2.2.2"
features = ["serde"]
[dependencies.serde]
version = "1.0"
version = "1.0.135"
features = ["derive"]
[dependencies.tokio]
version = "1.7.0"
version = "1.15.0"
features = ["macros"]

View File

@ -1,24 +0,0 @@
# this uses buster as alpine would always segfault
FROM rust:buster as builder
LABEL maintainer="LordMZTE <https://github.com/lordmzte>"
RUN apt install \
gcc \
libssl-dev
WORKDIR /usr/src/ruff
COPY Cargo.toml ./
COPY src/ src/
RUN cargo build --release
FROM debian:buster
COPY --from=builder /usr/src/ruff/target/release/ruff /usr/bin
RUN apt update
RUN apt install \
libssl1.1
VOLUME /ruffconfig
CMD /usr/bin/ruff -c /ruffconfig/config.toml

View File

@ -3,36 +3,46 @@ homeserver_url = "https://matrix.org"
user_id = "@ruffbot:matrix.org"
password = "xxx"
# path to store databases
store_path = "store"
store_path = "/var/cache/ruff"
# !sendmeme <id> to make ruff send the meme with the given id
sendmeme_command = "!sendmeme"
# MEMES!!
memes = [
# random stuff
{ keyword = "Xd", id = 1023, match = "contains", match_case = true },
{ 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 = "hmm", id = 892 },
{ keyword = "hmmm", id = 891 },
{ keyword = "itbyhf", randomcat = "itbyhf" },
{ keyword = "jens", id = 343 },
{ keyword = "jonasled", id = 164 },
{ keyword = "kappa", id = 182 },
{ keyword = "lordmzte", id = 315 },
{ keyword = "npm", id = 1080, match = "contains" },
{ keyword = "party", id = 619 },
{ keyword = "realtox", id = 168 },
{ keyword = "sklave", id = 304 },
{ keyword = "tilera", id = 316 },
{ keyword = "wtf", randomcat = "random", match = "contains" },
{ keyword = "wtuff", randomcat = "random", match = "contains" },
{ keyword = "xd", id = 1023, match = "contains", match_case = true },
# üffen
{ keyword = "uff", randomcat = "uff" },
{ keyword = "biguff", id = 771 },
{ keyword = "hmm", id = 892 },
{ keyword = "hmmm", id = 891 },
{ keyword = "longuff", id = 771 },
{ keyword = "uff2d", id = 1063 },
{ keyword = "uffag", id = 1061 },
{ keyword = "uffal", id = 654 },
{ keyword = "uffat", id = 257 },
{ keyword = "uffch", id = 286 },
{ keyword = "uffch", id = 283 },
{ keyword = "uffde", id = 144 },
{ keyword = "uffgo", id = 568 },
{ keyword = "uffhf", id = 645 },
@ -52,6 +62,7 @@ memes = [
{ keyword = "uffrt", id = 986 },
{ keyword = "uffru", id = 999 },
{ keyword = "uffsb", id = 818 },
{ keyword = "uffso", id = 1006 },
{ keyword = "uffsr", id = 585 },
{ keyword = "ufftl", id = 644 },
{ keyword = "uffwe", id = 779 },

9
ruff.service Normal file
View File

@ -0,0 +1,9 @@
[Unit]
Description=Next gen uff bot
[Service]
ExecStart=/usr/bin/ruff -c /etc/ruff/config.toml
Environment=RUST_LOG=information
[Install]
WantedBy=default.target

View File

@ -1,7 +1,8 @@
use crate::meme::Meme;
use crate::meme::MemeIdent;
use serde::de::{self, Deserializer, MapAccess, Visitor};
use serde::Deserialize;
use crate::meme::{Matcher, Meme, MemeIdent};
use serde::{
de::{self, Deserializer, MapAccess, Visitor},
Deserialize,
};
use std::path::PathBuf;
use url::Url;
@ -11,6 +12,7 @@ pub struct Config {
pub user_id: String,
pub password: String,
pub device_name: Option<String>,
pub sendmeme_command: Option<String>,
pub memes: Vec<Meme>,
pub store_path: PathBuf,
#[serde(default = "default_clear_threshold")]
@ -30,6 +32,8 @@ impl<'de> Deserialize<'de> for Meme {
enum Field {
Keyword,
Ident(IdentField),
Matcher,
MatchCase,
}
enum IdentField {
@ -61,6 +65,8 @@ impl<'de> Deserialize<'de> for Meme {
"keyword" => Field::Keyword,
"randomcat" => Field::Ident(IdentField::RandomCat),
"id" => Field::Ident(IdentField::Id),
"match" => Field::Matcher,
"match_case" => Field::MatchCase,
_ => return Err(de::Error::unknown_field(v, FIELDS)),
})
}
@ -84,6 +90,9 @@ impl<'de> Deserialize<'de> for Meme {
{
let mut keyword = None;
let mut ident = None;
let mut matcher = None;
let mut match_case = None;
while let Some(key) = map.next_key()? {
match key {
Field::Keyword => {
@ -92,7 +101,7 @@ impl<'de> Deserialize<'de> for Meme {
}
keyword = Some(map.next_value()?);
}
},
Field::Ident(i) => {
if ident.is_some() {
@ -104,17 +113,42 @@ impl<'de> Deserialize<'de> for Meme {
match i {
IdentField::Id => {
ident = Some(MemeIdent::Id(map.next_value()?));
}
},
IdentField::RandomCat => {
ident = Some(MemeIdent::RandomCat(map.next_value()?));
}
},
}
}
},
Field::Matcher => {
if matcher.is_some() {
return Err(de::Error::duplicate_field("match"));
}
matcher = Some(map.next_value()?);
},
Field::MatchCase => {
if match_case.is_some() {
return Err(de::Error::duplicate_field("match"));
}
match_case = Some(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 })
let matcher = matcher.unwrap_or(Matcher::Begins);
let match_case = match_case.unwrap_or(false);
Ok(Meme {
keyword,
ident,
matcher,
match_case,
})
}
}

View File

@ -1,25 +1,38 @@
use anyhow::{anyhow, bail, Context};
use jm_client_core::JMClient;
use libjens::JMClient;
use log::{error, info, warn};
use matrix_sdk::{
api::r0::session::login,
async_trait,
config::SyncSettings,
deserialized_responses::SyncResponse,
events::{
room::{
member::MemberEventContent,
message::{MessageEventContent, MessageType, TextMessageEventContent},
},
AnyToDeviceEvent, StrippedStateEvent, SyncMessageEvent,
},
encryption::verification::Verification,
room::Room,
verification::Verification,
EventHandler, LoopCtrl,
ruma::{
api::client::r0::{
session::login,
uiaa::{AuthData, Password, UserIdentifier},
},
assign,
events::{
room::{
member::StrippedRoomMemberEvent,
message::{
MessageType,
RoomMessageEventContent,
SyncRoomMessageEvent,
TextMessageEventContent,
},
},
AnyToDeviceEvent,
SyncMessageEvent,
},
UserId,
},
Client,
LoopCtrl,
};
use rand::{rngs::StdRng, SeedableRng};
use sled::Db;
use std::{
collections::BTreeMap,
path::PathBuf,
sync::{
atomic::{AtomicBool, AtomicU32},
@ -28,16 +41,14 @@ use std::{
time::Duration,
};
use structopt::StructOpt;
use tokio::sync::Mutex;
use tokio::sync::RwLock;
use tokio::sync::{Mutex, RwLock};
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;
mod util;
#[derive(Debug, StructOpt)]
struct Opt {
@ -60,37 +71,41 @@ async fn main() -> anyhow::Result<()> {
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())?));
let client = Client::new(config.homeserver_url.clone())?;
let device_name = config.device_name.as_ref().map(String::as_ref);
let login::Response { user_id, .. } = client
.read()
.await
.login(&config.user_id, &config.password, device_name, device_name)
.await?;
let bot = Arc::new(Bot {
client: client.clone(),
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())?),
});
client
.write()
.await
.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())?),
}))
.register_event_handler(on_stripped_state_member)
.await;
let bot_ = Arc::clone(&bot);
client
.register_event_handler(move |ev, client, room| {
on_room_message(ev, client, room, Arc::clone(&bot_))
})
.await;
let login::Response { user_id, .. } = client
.login(&config.user_id, &config.password, device_name, Some("ruff"))
.await?;
let initial = AtomicBool::from(true);
let initial_ref = &initial;
let client_ref = &client.read().await;
let client_ref = &client;
let config_ref = &config;
let user_id_ref = &user_id;
client
.read()
.await
.sync_with_callback(SyncSettings::new(), |response| async move {
if let Err(e) = on_response(&response, client_ref).await {
error!("Error processing response: {}", e);
@ -100,7 +115,7 @@ async fn main() -> anyhow::Result<()> {
if initial.load(std::sync::atomic::Ordering::SeqCst) {
if let Err(e) =
on_initial_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);
}
@ -116,88 +131,71 @@ async fn main() -> anyhow::Result<()> {
}
pub struct Bot {
client: Arc<RwLock<Client>>,
client: 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.
/// 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 EventHandler for Bot {
async fn on_stripped_state_member(
&self,
room: Room,
room_member: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
if room_member.state_key == self.client.read().await.user_id().await.unwrap() {
return;
}
if let Room::Invited(room) = room {
info!("Autojoining room {}", room.room_id());
let mut delay = 2;
while let Err(err) = self
.client
.read()
.await
.join_room_by_id(&room.room_id())
.await
{
// retry autojoin due to synapse sending invites, before the
// invited user can join for more information see
// https://github.com/matrix-org/synapse/issues/4345
warn!(
"Failed to join room {} ({:?}), retrying in {}s",
room.room_id(),
err,
delay
);
tokio::time::sleep(Duration::from_secs(delay)).await;
delay *= 2;
if delay > 3600 {
error!("Can't join room {} ({:?})", room.room_id(), err);
break;
}
}
info!("Successfully joined room {}", room.room_id());
}
async fn on_stripped_state_member(event: StrippedRoomMemberEvent, client: Client, room: Room) {
if event.state_key == client.user_id().await.unwrap() {
return;
}
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 Room::Invited(room) = room {
info!("Autojoining room {}", room.room_id());
let mut delay = 2;
while let Err(err) = client.join_room_by_id(room.room_id()).await {
// retry autojoin due to synapse sending invites, before the
// invited user can join for more information see
// https://github.com/matrix-org/synapse/issues/4345
warn!(
"Failed to join room {} ({:?}), retrying in {}s",
room.room_id(),
err,
delay
);
tokio::time::sleep(Duration::from_secs(delay)).await;
delay *= 2;
if delay > 3600 {
error!("Can't join room {} ({:?})", room.room_id(), err);
break;
}
}
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);
}
info!("Successfully joined room {}", room.room_id());
}
}
async fn on_room_message(msg: SyncRoomMessageEvent, client: Client, room: Room, bot: Arc<Bot>) {
if client
.user_id()
.await
.map(|u| u == msg.sender)
.unwrap_or(true)
{
return;
}
if let SyncMessageEvent {
content:
RoomMessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
..
},
..
} = msg
{
if let Err(e) = responder::on_msg(&msg_body, room, &bot).await {
error!("Responder error: {}", e);
}
}
}
@ -230,7 +228,7 @@ async fn on_response(response: &SyncResponse, client: &Client) -> anyhow::Result
error!("Error accepting key verification request: {}", e);
}
}
}
},
AnyToDeviceEvent::KeyVerificationKey(e) => {
if let Some(Verification::SasV1(sas)) = &client
@ -241,32 +239,15 @@ async fn on_response(response: &SyncResponse, client: &Client) -> anyhow::Result
error!("Error confirming key verification request: {}", e);
}
}
}
},
_ => {}
_ => {},
}
}
Ok(())
}
fn auth_data<'a>(user: &UserId, password: &str, session: Option<&'a str>) -> AuthData<'a> {
let mut auth_parameters = BTreeMap::new();
let identifier = json!({
"type": "m.id.user",
"user": user,
});
auth_parameters.insert("identifier".to_owned(), identifier);
auth_parameters.insert("password".to_owned(), password.to_owned().into());
AuthData::DirectRequest {
kind: "m.login.password",
auth_parameters,
session,
}
}
async fn bootstrap_cross_signing(
client: &Client,
user_id: &UserId,
@ -275,7 +256,11 @@ async fn bootstrap_cross_signing(
info!("bootstrapping e2e");
if let Err(e) = client.bootstrap_cross_signing(None).await {
if let Some(response) = e.uiaa_response() {
let auth_data = auth_data(&user_id, &password, response.session.as_deref());
let auth_data = AuthData::Password(assign!(
Password::new(UserIdentifier::MatrixId(user_id.as_str()), password),
{ session: response.session.as_deref() }
));
client
.bootstrap_cross_signing(Some(auth_data))
.await

View File

@ -1,33 +1,86 @@
use crate::Bot;
use log::error;
use rand::seq::IteratorRandom;
use serde::Deserialize;
pub const ALLOWED_SPACES: &str = " ,.;:!?({-_";
#[derive(Debug)]
pub struct Meme {
pub keyword: String,
pub ident: MemeIdent,
pub matcher: Matcher,
pub match_case: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Matcher {
Begins,
Contains,
}
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)
let mut msg = msg.to_string();
let mut keyword = self.keyword.clone();
if !self.match_case {
msg = msg.to_ascii_lowercase();
keyword = keyword.to_ascii_lowercase();
}
match self.matcher {
Matcher::Begins => {
msg.starts_with(&keyword) &&
// msg must have one of allowed chars after keyword
msg.chars().nth(keyword.len()).map(|c| ALLOWED_SPACES.contains(c)).unwrap_or(true)
},
Matcher::Contains => msg
.match_indices(&keyword)
.map(|(idx, subs)| {
(idx == 0 ||
msg.chars()
.nth(idx - 1)
.map(|c| ALLOWED_SPACES.contains(c))
.unwrap_or(true)) &&
msg.chars()
.nth(idx + subs.len())
.map(|c| ALLOWED_SPACES.contains(c))
.unwrap_or(true)
})
.any(|b| b),
}
}
pub async fn get_meme(&self, bot: &Bot) -> anyhow::Result<Option<jm_client_core::api::Meme>> {
pub async fn get_id(&self, bot: &Bot) -> anyhow::Result<Option<u32>> {
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()),
MemeIdent::Id(i) => Ok(Some(*i)),
MemeIdent::RandomCat(c) => {
let meme = memes
.iter()
.filter(|m| &m.category == c)
.choose(&mut *bot.rng.lock().await);
if let Some(meme) = meme {
let id = meme.id.parse::<u32>();
match id {
Err(e) => {
error!(
"Error parsing meme ID {} for meme {:?} thanks to tilera's PHP api",
&meme.id, &meme
);
Err(e.into())
},
Ok(id) => Ok(Some(id)),
}
} else {
Ok(None)
}
},
}
}
}
@ -37,3 +90,64 @@ pub enum MemeIdent {
RandomCat(String),
Id(u32),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_begins_test() {
let meme = Meme {
keyword: String::from("test"),
ident: MemeIdent::Id(42),
matcher: Matcher::Begins,
match_case: false,
};
assert!(!meme.matches("xxx"));
assert!(!meme.matches("testxxx"));
assert!(!meme.matches("xxxtestxxx"));
assert!(!meme.matches("xxxtest xxx"));
assert!(!meme.matches("xxx testxxx"));
assert!(meme.matches("test"));
assert!(meme.matches("test xxx"));
assert!(meme.matches("test; xxx"));
assert!(meme.matches("test;xxx"));
assert!(meme.matches("TEST"));
assert!(meme.matches("TeSt"));
}
#[test]
fn matches_contains_test() {
let meme = Meme {
keyword: String::from("test"),
ident: MemeIdent::Id(42),
matcher: Matcher::Contains,
match_case: false,
};
assert!(!meme.matches("xxx"));
assert!(!meme.matches("xxxtestxxx"));
assert!(!meme.matches("xxxtest xxx"));
assert!(!meme.matches("xxx testxxx"));
assert!(!meme.matches("xxxtest"));
assert!(!meme.matches("testxxx"));
assert!(meme.matches("xxx test xxx"));
assert!(meme.matches("xxx,test.xxx"));
assert!(meme.matches("xxx,TEST.xxx"));
assert!(meme.matches("xxx,TeSt.xxx"));
}
#[test]
fn matches_case_test() {
let meme = Meme {
keyword: String::from("TeSt"),
ident: MemeIdent::Id(42),
matcher: Matcher::Contains,
match_case: true,
};
assert!(!meme.matches("test"));
assert!(meme.matches("TeSt"));
}
}

View File

@ -1,54 +1,84 @@
use crate::Bot;
use crate::{util, Bot};
use anyhow::Context;
use log::{error, info, warn};
use matrix_sdk::{
api::r0::media::create_content,
events::{
room::message::{
ImageMessageEventContent, MessageEventContent, MessageType, VideoMessageEventContent,
},
AnyMessageEventContent,
},
identifiers::MxcUri,
room::{Joined, Room},
ruma::{
api::client::r0::media::create_content,
events::room::{
message::{
FileInfo,
FileMessageEventContent,
ImageMessageEventContent,
MessageType,
RoomMessageEventContent,
VideoInfo,
VideoMessageEventContent,
},
ImageInfo,
},
MxcUri,
UInt,
},
};
use mime::Mime;
use serde::{Deserialize, Serialize};
use std::io::Cursor;
use std::sync::atomic::Ordering;
use std::{io::Cursor, sync::atomic::Ordering};
/// A meme stored in the cache database
#[derive(Debug, Deserialize, Serialize)]
struct CachedMeme {
mxc: MxcUri,
ty: CachedMemeType,
/// mxc url of the meme
mxc: Box<MxcUri>,
/// MIME type of the meme
#[serde(with = "util::mime_serialize")]
mime: Mime,
/// file size of the meme
size: UInt,
/// file name of the meme
meme_name: String,
}
impl CachedMeme {
fn into_message_type(self, name: String) -> MessageType {
match self {
Self {
mxc,
ty: CachedMemeType::Image,
} => MessageType::Image(ImageMessageEventContent::plain(name, mxc, None)),
Self {
mxc,
ty: CachedMemeType::Video,
} => MessageType::Video(VideoMessageEventContent::plain(name, mxc, None)),
}
}
}
fn into_message_type(self) -> MessageType {
let Self {
mxc,
mime,
size,
meme_name,
} = self;
#[derive(Debug, Deserialize, Serialize)]
enum CachedMemeType {
Image,
Video,
}
impl CachedMemeType {
fn from_mime_name(name: &mime::Name) -> Option<Self> {
match *name {
mime::VIDEO => Some(Self::Video),
mime::IMAGE => Some(Self::Image),
_ => None,
match mime.type_() {
mime::IMAGE => MessageType::Image(ImageMessageEventContent::plain(
meme_name,
mxc,
Some(Box::new({
let mut info = ImageInfo::new();
info.mimetype = Some(mime.to_string());
info.size = Some(size);
info
})),
)),
mime::VIDEO => MessageType::Video(VideoMessageEventContent::plain(
meme_name,
mxc,
Some(Box::new({
let mut info = VideoInfo::new();
info.mimetype = Some(mime.to_string());
info.size = Some(size);
info
})),
)),
_ => MessageType::File(FileMessageEventContent::plain(
meme_name,
mxc,
Some(Box::new({
let mut info = FileInfo::new();
info.mimetype = Some(mime.to_string());
info.size = Some(size);
info
})),
)),
}
}
}
@ -63,86 +93,119 @@ pub async fn on_msg(msg: &str, room: Room, bot: &Bot) -> anyhow::Result<()> {
room.name()
);
return Ok(());
}
},
};
if let Some(ref sendmeme_cmd) = bot.config.sendmeme_command {
let mut words = msg.split(' ');
if words.next() == Some(sendmeme_cmd) {
if let Some(id) = words.next() {
let id = if let Ok(id) = id.parse::<u32>() {
id
} else {
room.send(RoomMessageEventContent::text_plain("Invalid ID!"), None)
.await?;
return Ok(());
};
bot.jm_client.write().await.clear_cache().await;
bot.meme_count.store(0, Ordering::SeqCst);
cache_send_meme(id, bot, room).await?;
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())? {
let cached = bincode::deserialize::<CachedMeme>(&ivec)?;
send_meme(&room, cached, 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 ty = CachedMemeType::from_mime_name(&mime.type_())
.context("Found meme that is neither video nor image!")?;
let create_content::Response { content_uri, .. } = bot
.client
.read()
.await
.upload(&mime, &mut Cursor::new(resp))
.await?;
let cached = CachedMeme {
mxc: content_uri,
ty,
};
bot.memecache
.insert(id.to_le_bytes(), bincode::serialize(&cached)?)?;
send_meme(&room, cached, meme_name.clone()).await?;
} else {
error!(
"Couldn't guess MIME type of meme '{}', skipping.",
&meme.link
);
}
}
}
}
} else {
error!("Found meme with invalid id! {:?}", &meme);
if let Some(id) = meme.get_id(bot).await? {
cache_send_meme(id, bot, room).await?;
}
break;
}
}
Ok(())
}
async fn send_meme(room: &Joined, cached: CachedMeme, meme_name: String) -> anyhow::Result<()> {
let msg_ty = cached.into_message_type(meme_name);
async fn cache_send_meme(meme_id: u32, bot: &Bot, room: Joined) -> anyhow::Result<()> {
bot.meme_count.fetch_add(1, Ordering::SeqCst);
let memes = bot.jm_client.read().await.get_memes().await?;
let meme = memes
.iter()
.find(|m| m.id.parse::<u32>().ok() == Some(meme_id))
.cloned();
room.send(
AnyMessageEventContent::RoomMessage(MessageEventContent::new(msg_ty)),
None,
)
.await
.context("Failed to send meme")?;
if let Some(meme) = meme {
if let Some(ivec) = bot.memecache.get(meme_id.to_be_bytes())? {
let cached = bincode::deserialize::<CachedMeme>(&ivec)?;
send_meme(&room, cached).await?;
} else {
info!("Meme {} not found in cache, uploading...", meme_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 size = resp.len();
let create_content::Response { content_uri, .. } =
bot.client.upload(&mime, &mut Cursor::new(resp)).await?;
let cached = CachedMeme {
mxc: content_uri,
mime,
size: UInt::new(size as u64)
.context("Meme has file size over allowed limit!")?,
meme_name: meme
.link
.split('/')
.last()
.unwrap_or(&meme.link)
.to_string(),
};
bot.memecache
.insert(meme_id.to_be_bytes(), bincode::serialize(&cached)?)?;
send_meme(&room, cached).await?;
// we do this after we have responded, in order to not delay the response
if bot.meme_count.load(Ordering::SeqCst) >= bot.config.clear_cache_threshold {
let mut client = bot.jm_client.write().await;
bot.meme_count.store(0, Ordering::SeqCst);
client.clear_cache().await;
// memes requested but not used, but they will be cached
client.get_memes().await?;
}
} else {
error!(
"Couldn't guess MIME type of meme '{}', skipping.",
&meme.link
);
}
}
} else {
room.send(
RoomMessageEventContent::text_plain(format!("No meme with id '{}'", meme_id)),
None,
)
.await?;
}
Ok(())
}
async fn send_meme(room: &Joined, cached: CachedMeme) -> anyhow::Result<()> {
let msg_ty = cached.into_message_type();
room.send(RoomMessageEventContent::new(msg_ty), None)
.await
.context("Failed to send meme")?;
Ok(())
}

39
src/util.rs Normal file
View File

@ -0,0 +1,39 @@
pub mod mime_serialize {
use mime::Mime;
use serde::{
de::{self, Unexpected, Visitor},
Deserializer,
Serializer,
};
pub fn serialize<S>(data: &Mime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(data.as_ref())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Mime, D::Error>
where
D: Deserializer<'de>,
{
struct Vis;
impl<'de> Visitor<'de> for Vis {
type Value = Mime;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a MIME type")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
v.parse()
.map_err(|_| de::Error::invalid_value(Unexpected::Str(v), &Vis))
}
}
deserializer.deserialize_str(Vis)
}
}