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,
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
3
ui/src/components/symbols.tsx
vendored
3
ui/src/components/symbols.tsx
vendored
|
@ -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>
|
||||
|
|
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