Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds sentiment analysis to posts. #38

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ ring = { version = '^0.16.0', default-features = false, features = ['std'] }
serde = { version = '^1.0.139', features = ['derive'] }
serde_json = '^1.0.82'
serde_with = '^1.14.0'
vader_sentiment = "0.1.1"
38 changes: 38 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,44 @@ paths:
BadRequest:
type: string

/posts/{post_id}/sentiment:
get:
summary: Calculates the sentiment of a post via masked ID.
parameters:
- name: post_id
in: path
required: true
schema:
id:
type: string
responses:
'200':
content:
application/json:
schema:
type: object
properties:
positive:
type: number
example: 0.2241094597782496
negative:
type: number
example: 0.44169222300857125
neutral:
type: number
example: 0.3341983172131792
'400':
content:
application/json:
schema:
type: 'object'
properties:
BadRequest:
type: string
'500':
$ref: '#/components/responses/unexpected'


/posts/{post_id}/:
get:
summary: Get a singular post via masked post ID.
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
.service(services::posts::create)
.service(services::posts::list)
.service(services::posts::vote)
.service(services::posts::sentiment_analysis)
.service(services::profile::update_profile)
.service(services::profile::get_profile)
.service(services::posts::get_single_post)
Expand Down
53 changes: 52 additions & 1 deletion src/services/posts.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::convert::TryFrom;
extern crate vader_sentiment;

use actix_web::{
get,
Expand Down Expand Up @@ -107,7 +108,7 @@ pub async fn get_single_post(
// Query the database for the post.
let possible_post = db.collection::<Post>("posts").find_one(doc! {"_id": post_id}, None).await;
let post: Post;
// Return 400 if the post doesn't exist, 500 if there's a query error, or the [`Detail`] post itself
// Return 400 if the post doesn't exist, 500 if there's a query error, or the [`Detail`] post itself
// if everything works.
match possible_post {
Ok(possible_post) => match possible_post {
Expand Down Expand Up @@ -456,3 +457,53 @@ pub async fn vote(

success(votes)
}

#[derive(Serialize)]
pub struct Sentiment {
pub positive: f64,
pub negative: f64,
pub neutral: f64,
}

/// Route for calculating the sentiment analysis of a post via a specific masked ID.
///
/// Mainly exists for the sake of existing. AKA, most users won't use it, and it's trivial, but it adds context
/// to the app, allowing users to click something new when they get bored.
/// TODO: Look into citing the `vader_sentiment` tool.
#[get("/posts/{post_id}/sentiment")]
pub async fn sentiment_analysis(
db: web::Data<Database>,
masking_key: web::Data<&'static MaskingKey>,
post_id: web::Path<MaskedObjectId>
) -> ApiResult<Sentiment, ()> {
// Unmask the ID, in order for it to be used for querying.
let post_id = masking_key.unmask(&post_id)
.map_err(|masked_oid::PaddingError| Failure::BadRequest("bad masked id"))?;
// Query the database for the post.
let possible_post = db.collection::<Post>("posts").find_one(doc! {"_id": post_id}, None).await;
let post: Post;
// Return 400 if the post doesn't exist, 500 if there's a query error, or an `Sentiment`
// if everything works.
match possible_post {
Ok(possible_post) => match possible_post {
Some(found_post) => post = found_post,
None => return Err(Failure::BadRequest("no post found for this id")),
},
Err(_) => return Err(Failure::Unexpected),
};
let analyzer = vader_sentiment::SentimentIntensityAnalyzer::new();
let scores = analyzer.polarity_scores(&post.text);
let positive = match scores.get("pos") {
None => return Err(Failure::Unexpected),
Some(positive) => positive,
};
let neutral = match scores.get("neu") {
None => return Err(Failure::Unexpected),
Some(neutral) => neutral,
};
let negative = match scores.get("neg") {
None => return Err(Failure::Unexpected),
Some(negative) => negative,
};
success(Sentiment { positive: *positive, negative: *negative, neutral: *neutral})
}