Adding audio wav file for Captcha using espeak.

This commit is contained in:
Dessalines 2020-07-26 17:24:25 -04:00
parent cde76af4aa
commit 1e1e87c9b2
9 changed files with 116 additions and 18 deletions

View file

@ -41,6 +41,9 @@ FROM alpine:3.12
# Install libpq for postgres
RUN apk add libpq
# Install Espeak for captchas
RUN apk add espeak
# Copy resources
COPY server/config/defaults.hjson /config/defaults.hjson
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/debug/lemmy_server /app/lemmy

View file

@ -50,6 +50,10 @@ FROM alpine:3.12 as lemmy
# Install libpq for postgres
RUN apk add libpq
# Install Espeak for captchas
RUN apk add espeak
RUN addgroup -g 1000 lemmy
RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy

View file

@ -428,6 +428,7 @@ These expire after 10 minutes.
op: "GetCaptcha",
data: {
png: String, // A Base64 encoded png
wav: Option<String>, // A Base64 encoded wav audio file
uuid: String,
}
}

View file

@ -2,6 +2,7 @@ use crate::{
api::{claims::Claims, is_admin, APIError, Oper, Perform},
apub::ApubObjectType,
blocking,
captcha_espeak_wav_base64,
websocket::{
server::{CaptchaItem, CheckCaptcha, JoinUserRoom, SendAllMessage, SendUserRoomMessage},
UserOperation,
@ -79,7 +80,8 @@ pub struct GetCaptcha {}
#[derive(Serialize, Deserialize)]
pub struct GetCaptchaResponse {
png: String, // A Base64 encoded png
png: String, // A Base64 encoded png
wav: Option<String>, // A Base64 encoded wav audio
uuid: String,
}
@ -513,6 +515,8 @@ impl Perform for Oper<GetCaptcha> {
let uuid = uuid::Uuid::new_v4().to_string();
let wav = captcha_espeak_wav_base64(&answer).ok();
let captcha_item = CaptchaItem {
answer,
uuid: uuid.to_owned(),
@ -523,7 +527,7 @@ impl Perform for Oper<GetCaptcha> {
ws.chatserver.do_send(captcha_item);
}
Ok(GetCaptchaResponse { png, uuid })
Ok(GetCaptchaResponse { png, uuid, wav })
}
}

View file

@ -36,6 +36,7 @@ use actix_web::{client::Client, dev::ConnectionInfo};
use log::error;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
use std::process::Command;
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
pub type ConnectionId = usize;
@ -213,9 +214,56 @@ where
Ok(res)
}
pub fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
let mut built_text = String::new();
// Building proper speech text for espeak
for mut c in captcha.chars() {
let new_str = if c.is_alphabetic() {
if c.is_lowercase() {
c.make_ascii_uppercase();
format!("lower case {} ... ", c)
} else {
c.make_ascii_uppercase();
format!("capital {} ... ", c)
}
} else {
format!("{} ...", c)
};
built_text.push_str(&new_str);
}
espeak_wav_base64(&built_text)
}
pub fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
// Make a temp file path
let uuid = uuid::Uuid::new_v4().to_string();
let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid);
// Write the wav file
Command::new("espeak")
.arg("-w")
.arg(&file_path)
.arg(text)
.status()?;
// Read the wav file bytes
let bytes = std::fs::read(&file_path)?;
// Delete the file
std::fs::remove_file(file_path)?;
// Convert to base64
let base64 = base64::encode(bytes);
Ok(base64)
}
#[cfg(test)]
mod tests {
use crate::is_image_content_type;
use crate::{captcha_espeak_wav_base64, is_image_content_type};
#[test]
fn test_image() {
@ -230,6 +278,11 @@ mod tests {
});
}
#[test]
fn test_espeak() {
assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
}
// These helped with testing
// #[test]
// fn test_iframely() {

View file

@ -49,6 +49,7 @@ export class Login extends Component<any, State> {
captcha: {
png: undefined,
uuid: undefined,
wav: undefined,
},
};
@ -136,11 +137,7 @@ export class Login extends Component<any, State> {
<label class="col-sm-2" htmlFor="login-captcha">
{i18n.t('enter_code')}
</label>
<div class="col-sm-4">
{this.state.captcha.uuid && (
<img class="rounded img-fluid" src={this.captchaSrc()} />
)}
</div>
{this.showCaptcha()}
<div class="col-sm-6">
<input
type="text"
@ -169,6 +166,7 @@ export class Login extends Component<any, State> {
</div>
);
}
registerForm() {
return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
@ -259,11 +257,7 @@ export class Login extends Component<any, State> {
<label class="col-sm-2" htmlFor="register-captcha">
{i18n.t('enter_code')}
</label>
<div class="col-sm-4">
{this.state.captcha.uuid && (
<img class="rounded img-fluid" src={this.captchaSrc()} />
)}
</div>
{this.showCaptcha()}
<div class="col-sm-6">
<input
type="text"
@ -310,6 +304,34 @@ export class Login extends Component<any, State> {
);
}
showCaptcha() {
return (
<div class="col-sm-4">
{this.state.captcha.uuid && (
<>
<img
class="rounded-top img-fluid"
src={this.captchaPngSrc()}
style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
/>
{this.state.captcha.wav && (
<button
class="rounded-bottom btn btn-sm btn-secondary btn-block"
style="border-top-right-radius: 0; border-top-left-radius: 0;"
title={i18n.t('play_captcha_audio')}
onClick={linkEvent(this, this.handleCaptchaPlay)}
>
<svg class="icon icon-play">
<use xlinkHref="#icon-play"></use>
</svg>
</button>
)}
</>
)}
</div>
);
}
handleLoginSubmit(i: Login, event: any) {
event.preventDefault();
i.state.loginLoading = true;
@ -380,7 +402,13 @@ export class Login extends Component<any, State> {
WebSocketService.Instance.passwordReset(resetForm);
}
captchaSrc() {
handleCaptchaPlay(i: Login) {
event.preventDefault();
let snd = new Audio('data:audio/wav;base64,' + i.state.captcha.wav);
snd.play();
}
captchaPngSrc() {
return `data:image/png;base64,${this.state.captcha.png}`;
}
@ -412,7 +440,7 @@ export class Login extends Component<any, State> {
this.state.captcha = data;
this.state.loginForm.captcha_uuid = data.uuid;
this.state.registerForm.captcha_uuid = data.uuid;
this.setState({ captcha: data });
this.setState(this.state);
} else if (res.op == UserOperation.PasswordReset) {
toast(i18n.t('reset_password_mail_sent'));
} else if (res.op == UserOperation.GetSite) {

File diff suppressed because one or more lines are too long

View file

@ -566,6 +566,7 @@ export interface RegisterForm {
export interface GetCaptchaResponse {
png: string;
wav?: string;
uuid: string;
}

View file

@ -283,5 +283,6 @@
"cake_day_title": "Cake day:",
"cake_day_info": "It's {{ creator_name }}'s cake day today!",
"invalid_post_title": "Invalid post title",
"invalid_url": "Invalid URL."
"invalid_url": "Invalid URL.",
"play_captcha_audio": "Play Captcha Audio"
}