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,
@ -80,6 +81,7 @@ pub struct GetCaptcha {}
#[derive(Serialize, Deserialize)]
pub struct GetCaptchaResponse {
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) {

View file

@ -15,6 +15,9 @@ export class Symbols extends Component<any, any> {
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<symbol id="icon-play" viewBox="0 0 24 24">
<path d="M5.541 2.159c-0.153-0.1-0.34-0.159-0.541-0.159-0.552 0-1 0.448-1 1v18c-0.001 0.182 0.050 0.372 0.159 0.541 0.299 0.465 0.917 0.599 1.382 0.3l14-9c0.114-0.072 0.219-0.174 0.3-0.3 0.299-0.465 0.164-1.083-0.3-1.382zM6 4.832l11.151 7.168-11.151 7.168z"></path>
</symbol>
<symbol id="icon-strikethrough" viewBox="0 0 28 28">
<path d="M27.5 14c0.281 0 0.5 0.219 0.5 0.5v1c0 0.281-0.219 0.5-0.5 0.5h-27c-0.281 0-0.5-0.219-0.5-0.5v-1c0-0.281 0.219-0.5 0.5-0.5h27zM7.547 13c-0.297-0.375-0.562-0.797-0.797-1.25-0.5-1.016-0.75-2-0.75-2.938 0-1.906 0.703-3.5 2.094-4.828s3.437-1.984 6.141-1.984c0.594 0 1.453 0.109 2.609 0.297 0.688 0.125 1.609 0.375 2.766 0.75 0.109 0.406 0.219 1.031 0.328 1.844 0.141 1.234 0.219 2.187 0.219 2.859 0 0.219-0.031 0.453-0.078 0.703l-0.187 0.047-1.313-0.094-0.219-0.031c-0.531-1.578-1.078-2.641-1.609-3.203-0.922-0.953-2.031-1.422-3.281-1.422-1.188 0-2.141 0.313-2.844 0.922s-1.047 1.375-1.047 2.281c0 0.766 0.344 1.484 1.031 2.188s2.141 1.375 4.359 2.016c0.75 0.219 1.641 0.562 2.703 1.031 0.562 0.266 1.062 0.531 1.484 0.812h-11.609zM15.469 17h6.422c0.078 0.438 0.109 0.922 0.109 1.437 0 1.125-0.203 2.234-0.641 3.313-0.234 0.578-0.594 1.109-1.109 1.625-0.375 0.359-0.938 0.781-1.703 1.266-0.781 0.469-1.563 0.828-2.391 1.031-0.828 0.219-1.875 0.328-3.172 0.328-0.859 0-1.891-0.031-3.047-0.359l-2.188-0.625c-0.609-0.172-0.969-0.313-1.125-0.438-0.063-0.063-0.125-0.172-0.125-0.344v-0.203c0-0.125 0.031-0.938-0.031-2.438-0.031-0.781 0.031-1.328 0.031-1.641v-0.688l1.594-0.031c0.578 1.328 0.844 2.125 1.016 2.406 0.375 0.609 0.797 1.094 1.25 1.469s1 0.672 1.641 0.891c0.625 0.234 1.328 0.344 2.063 0.344 0.656 0 1.391-0.141 2.172-0.422 0.797-0.266 1.437-0.719 1.906-1.344 0.484-0.625 0.734-1.297 0.734-2.016 0-0.875-0.422-1.687-1.266-2.453-0.344-0.297-1.062-0.672-2.141-1.109z"></path>
</symbol>

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"
}