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
|
# Install libpq for postgres
|
||||||
RUN apk add libpq
|
RUN apk add libpq
|
||||||
|
|
||||||
|
# Install Espeak for captchas
|
||||||
|
RUN apk add espeak
|
||||||
|
|
||||||
# Copy resources
|
# Copy resources
|
||||||
COPY server/config/defaults.hjson /config/defaults.hjson
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/debug/lemmy_server /app/lemmy
|
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
|
# Install libpq for postgres
|
||||||
RUN apk add libpq
|
RUN apk add libpq
|
||||||
|
|
||||||
|
# Install Espeak for captchas
|
||||||
|
RUN apk add espeak
|
||||||
|
|
||||||
RUN addgroup -g 1000 lemmy
|
RUN addgroup -g 1000 lemmy
|
||||||
RUN adduser -D -s /bin/sh -u 1000 -G lemmy 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",
|
op: "GetCaptcha",
|
||||||
data: {
|
data: {
|
||||||
png: String, // A Base64 encoded png
|
png: String, // A Base64 encoded png
|
||||||
|
wav: Option<String>, // A Base64 encoded wav audio file
|
||||||
uuid: String,
|
uuid: String,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ use crate::{
|
||||||
api::{claims::Claims, is_admin, APIError, Oper, Perform},
|
api::{claims::Claims, is_admin, APIError, Oper, Perform},
|
||||||
apub::ApubObjectType,
|
apub::ApubObjectType,
|
||||||
blocking,
|
blocking,
|
||||||
|
captcha_espeak_wav_base64,
|
||||||
websocket::{
|
websocket::{
|
||||||
server::{CaptchaItem, CheckCaptcha, JoinUserRoom, SendAllMessage, SendUserRoomMessage},
|
server::{CaptchaItem, CheckCaptcha, JoinUserRoom, SendAllMessage, SendUserRoomMessage},
|
||||||
UserOperation,
|
UserOperation,
|
||||||
|
@ -79,7 +80,8 @@ pub struct GetCaptcha {}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetCaptchaResponse {
|
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,
|
uuid: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -513,6 +515,8 @@ impl Perform for Oper<GetCaptcha> {
|
||||||
|
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
let uuid = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let wav = captcha_espeak_wav_base64(&answer).ok();
|
||||||
|
|
||||||
let captcha_item = CaptchaItem {
|
let captcha_item = CaptchaItem {
|
||||||
answer,
|
answer,
|
||||||
uuid: uuid.to_owned(),
|
uuid: uuid.to_owned(),
|
||||||
|
@ -523,7 +527,7 @@ impl Perform for Oper<GetCaptcha> {
|
||||||
ws.chatserver.do_send(captcha_item);
|
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 log::error;
|
||||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
|
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
|
||||||
pub type ConnectionId = usize;
|
pub type ConnectionId = usize;
|
||||||
|
@ -213,9 +214,56 @@ where
|
||||||
Ok(res)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::is_image_content_type;
|
use crate::{captcha_espeak_wav_base64, is_image_content_type};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_image() {
|
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
|
// These helped with testing
|
||||||
// #[test]
|
// #[test]
|
||||||
// fn test_iframely() {
|
// 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: {
|
captcha: {
|
||||||
png: undefined,
|
png: undefined,
|
||||||
uuid: undefined,
|
uuid: undefined,
|
||||||
|
wav: undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -136,11 +137,7 @@ export class Login extends Component<any, State> {
|
||||||
<label class="col-sm-2" htmlFor="login-captcha">
|
<label class="col-sm-2" htmlFor="login-captcha">
|
||||||
{i18n.t('enter_code')}
|
{i18n.t('enter_code')}
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-4">
|
{this.showCaptcha()}
|
||||||
{this.state.captcha.uuid && (
|
|
||||||
<img class="rounded img-fluid" src={this.captchaSrc()} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -169,6 +166,7 @@ export class Login extends Component<any, State> {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerForm() {
|
registerForm() {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
|
<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">
|
<label class="col-sm-2" htmlFor="register-captcha">
|
||||||
{i18n.t('enter_code')}
|
{i18n.t('enter_code')}
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-4">
|
{this.showCaptcha()}
|
||||||
{this.state.captcha.uuid && (
|
|
||||||
<img class="rounded img-fluid" src={this.captchaSrc()} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<input
|
<input
|
||||||
type="text"
|
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) {
|
handleLoginSubmit(i: Login, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.state.loginLoading = true;
|
i.state.loginLoading = true;
|
||||||
|
@ -380,7 +402,13 @@ export class Login extends Component<any, State> {
|
||||||
WebSocketService.Instance.passwordReset(resetForm);
|
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}`;
|
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.captcha = data;
|
||||||
this.state.loginForm.captcha_uuid = data.uuid;
|
this.state.loginForm.captcha_uuid = data.uuid;
|
||||||
this.state.registerForm.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) {
|
} else if (res.op == UserOperation.PasswordReset) {
|
||||||
toast(i18n.t('reset_password_mail_sent'));
|
toast(i18n.t('reset_password_mail_sent'));
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} 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 {
|
export interface GetCaptchaResponse {
|
||||||
png: string;
|
png: string;
|
||||||
|
wav?: string;
|
||||||
uuid: 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_title": "Cake day:",
|
||||||
"cake_day_info": "It's {{ creator_name }}'s cake day today!",
|
"cake_day_info": "It's {{ creator_name }}'s cake day today!",
|
||||||
"invalid_post_title": "Invalid post title",
|
"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