Adding audio wav file for Captcha using espeak.
This commit is contained in:
parent
cde76af4aa
commit
1e1e87c9b2
9 changed files with 116 additions and 18 deletions
3
docker/dev/Dockerfile
vendored
3
docker/dev/Dockerfile
vendored
|
@ -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
|
||||
|
|
4
docker/prod/Dockerfile
vendored
4
docker/prod/Dockerfile
vendored
|
@ -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
|
||||
|
||||
|
|
1
docs/src/contributing_websocket_http_api.md
vendored
1
docs/src/contributing_websocket_http_api.md
vendored
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
52
ui/src/components/login.tsx
vendored
52
ui/src/components/login.tsx
vendored
|
@ -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) {
|
||||
|
|
7
ui/src/components/symbols.tsx
vendored
7
ui/src/components/symbols.tsx
vendored
File diff suppressed because one or more lines are too long
1
ui/src/interfaces.ts
vendored
1
ui/src/interfaces.ts
vendored
|
@ -566,6 +566,7 @@ export interface RegisterForm {
|
|||
|
||||
export interface GetCaptchaResponse {
|
||||
png: string;
|
||||
wav?: string;
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
|
|
3
ui/translations/en.json
vendored
3
ui/translations/en.json
vendored
|
@ -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"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue