Skip to content

Commit

Permalink
Merge pull request #268 from NEU-DSG/cm/2023-04-06-contributor-audio
Browse files Browse the repository at this point in the history
[DP-186; 200] Contributor audio upload; editor audio curation
  • Loading branch information
GracefulLemming authored Aug 17, 2023
2 parents d1d4b81 + 6065869 commit c5b00ee
Show file tree
Hide file tree
Showing 30 changed files with 43,713 additions and 1,384 deletions.
40 changes: 39 additions & 1 deletion graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,21 @@ Element within a spreadsheet before being transformed into a full document.
"""
union AnnotatedSeg = AnnotatedForm | LineBreak

"""
Request to attach user-recorded audio to a word
"""
input AttachAudioToWordInput {
"""
Word to bind audio to
"""
wordId: UUID!
"""
A URL to a Cloudfront-proxied user-recorded pronunciation of a word.
A new resource will be created to represent the recording if one does not exist already
"""
contributorAudioUrl: String!
}

"""
A segment of audio representing a document, word, phrase,
or other audio unit
Expand Down Expand Up @@ -342,6 +357,24 @@ type ContributorDetails {
birthDate: Date
}

"""
Request to update if a piece of audio should be included in an edited collection
"""
input CurateWordAudioInput {
"""
Word audio is attached to
"""
wordId: UUID!
"""
Audio to include/exclude
"""
audioSliceId: UUID!
"""
New value
"""
includeInEditedCollection: Boolean!
}

type Date {
"""
The year of this date
Expand Down Expand Up @@ -613,7 +646,9 @@ type Mutation {
updateParagraph(paragraph: ParagraphUpdate!): UUID!
updatePage(data: JSON!): Boolean!
updateAnnotation(data: JSON!): Boolean!
updateWord(word: AnnotatedFormUpdate!): UUID!
updateWord(word: AnnotatedFormUpdate!): AnnotatedForm!
curateWordAudio(input: CurateWordAudioInput!): AnnotatedForm!
attachAudioToWord(input: AttachAudioToWordInput!): AnnotatedForm!
}

"""
Expand Down Expand Up @@ -806,6 +841,9 @@ enum UserGroup {
EDITORS
}

"""
Auth metadata on the user making the current request.
"""
type UserInfo {
"""
Unique ID for the User. Should be an AWS Cognito Sub.
Expand Down
4 changes: 4 additions & 0 deletions graphql/src/lambda.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ async fn handler(
_ => None,
};

if let Some(user_id) = user.as_ref().map(|u| u.id) {
database.upsert_dailp_user(user_id).await?;
}

// TODO Hook up warp or tide instead of using a manual conditional.
let path = req.uri().path();
// GraphQL queries route to the /graphql endpoint.
Expand Down
67 changes: 61 additions & 6 deletions graphql/src/query.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
//! This piece of the project exposes a GraphQL endpoint that allows one to access DAILP data in a federated manner with specific queries.

use dailp::{slugify_ltree, CollectionChapter, Uuid};
use dailp::{
slugify_ltree, AnnotatedForm, AttachAudioToWordInput, CollectionChapter, CurateWordAudioInput,
Uuid,
};
use itertools::Itertools;

use {
dailp::async_graphql::{self, dataloader::DataLoader, Context, FieldResult, Guard},
dailp::async_graphql::{self, dataloader::DataLoader, Context, FieldResult, Guard, Object},
dailp::{
AnnotatedDoc, AnnotatedFormUpdate, CherokeeOrthography, Database, EditedCollection,
MorphemeId, MorphemeReference, MorphemeTag, ParagraphUpdate, WordsInDocument,
Expand Down Expand Up @@ -350,11 +353,62 @@ impl Mutation {
&self,
context: &Context<'_>,
word: AnnotatedFormUpdate,
) -> FieldResult<Uuid> {
) -> FieldResult<AnnotatedForm> {
let database = context.data::<DataLoader<Database>>()?.loader();
Ok(database
.word_by_id(&database.update_word(word).await?)
.await?)
}

/// Decide if a piece audio should be included in edited collection
#[graphql(guard = "GroupGuard::new(UserGroup::Editors)")]
async fn curate_word_audio(
&self,
context: &Context<'_>,
input: CurateWordAudioInput,
) -> FieldResult<dailp::AnnotatedForm> {
// TODO: should this return a typed id ie. AudioSliceId?
let user = context
.data_opt::<UserInfo>()
.ok_or_else(|| anyhow::format_err!("User is not signed in"))?;
let word_id = context
.data::<DataLoader<Database>>()?
.loader()
.update_audio_visibility(
&input.word_id,
&input.audio_slice_id,
input.include_in_edited_collection,
&user.id,
)
.await?;
Ok(context
.data::<DataLoader<Database>>()?
.loader()
.word_by_id(&word_id.ok_or_else(|| anyhow::format_err!("Word audio not found"))?)
.await?)
}

/// Attach audio that has already been uploaded to S3 to a particular word
/// Assumes user requesting mutation recoreded the audio
#[graphql(guard = "GroupGuard::new(UserGroup::Contributors)")]
async fn attach_audio_to_word(
&self,
context: &Context<'_>,
input: AttachAudioToWordInput,
) -> FieldResult<dailp::AnnotatedForm> {
// TODO: should this return a typed id ie. AudioSliceId?
let user = context
.data_opt::<UserInfo>()
.ok_or_else(|| anyhow::format_err!("User is not signed in"))?;
let word_id = context
.data::<DataLoader<Database>>()?
.loader()
.attach_audio_to_word(input, &user.id)
.await?;
Ok(context
.data::<DataLoader<Database>>()?
.loader()
.update_word(word)
.word_by_id(&word_id)
.await?)
}
}
Expand All @@ -366,19 +420,20 @@ struct FormsInTime {
forms: Vec<dailp::AnnotatedForm>,
}

/// Auth metadata on the user making the current request.
#[derive(Deserialize, Debug, async_graphql::SimpleObject)]
pub struct UserInfo {
/// Unique ID for the User. Should be an AWS Cognito Sub.
#[serde(default, rename = "sub")]
id: Uuid,
pub id: Uuid,
email: String,
#[serde(default, rename = "cognito:groups")]
groups: Vec<UserGroup>,
}
impl UserInfo {
pub fn new_test_admin() -> Self {
Self {
id: Uuid::parse_str("a0a9e9e6-a37a-4d09-bd4b-86b5e57be31a").unwrap(),
id: Uuid::parse_str("5f22a8bf-46c8-426c-a104-b4faf7c2d608").unwrap(),
email: "test@dailp.northeastern.edu".to_string(),
groups: vec![UserGroup::Editors],
}
Expand Down
18 changes: 14 additions & 4 deletions graphql/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ async fn main() -> tide::Result<()> {

// add tide endpoint
app.at("/graphql").post(async_graphql_tide::graphql(schema));
app.at("/graphql-edit")
.post(AuthedEndpoint::new(authed_schema));
app.at("/graphql-edit").post(AuthedEndpoint::new(
authed_schema,
dailp::Database::connect(None)?,
));

// enable graphql playground
app.at("/graphql").get(|_| async move {
Expand Down Expand Up @@ -78,11 +80,15 @@ async fn main() -> tide::Result<()> {

struct AuthedEndpoint {
schema: Schema<query::Query, query::Mutation, EmptySubscription>,
database: dailp::Database,
}

impl AuthedEndpoint {
fn new(schema: Schema<query::Query, query::Mutation, EmptySubscription>) -> Self {
Self { schema }
fn new(
schema: Schema<query::Query, query::Mutation, EmptySubscription>,
database: dailp::Database,
) -> Self {
Self { schema, database }
}
}

Expand All @@ -103,6 +109,10 @@ impl Endpoint<()> for AuthedEndpoint {
},
);

if let Some(user_id) = user.as_ref().map(|u| u.id) {
self.database.upsert_dailp_user(user_id).await?;
}

let req = async_graphql_tide::receive_request(req).await?;
let req = if let Some(user) = user {
req.data(user)
Expand Down
Loading

0 comments on commit c5b00ee

Please sign in to comment.