From 82329835b27b9daf7c84256640c4899788f392d8 Mon Sep 17 00:00:00 2001 From: piaoliu <441594700@qq.com> Date: Mon, 2 Apr 2018 23:06:51 +0800 Subject: [PATCH 1/3] add github view and redis lrem function --- src/util/redis_pool.rs | 14 ++++++++++++++ static/js/admin/article_list.js | 4 ++-- static/js/admin/ip_list.js | 2 +- static/js/admin/user_list.js | 2 +- views/admin/article_view.html | 2 +- views/admin/users.html | 3 ++- views/template/user_list.html | 1 + views/visitor/user_info.html | 1 + 8 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/util/redis_pool.rs b/src/util/redis_pool.rs index 888e836..8423adc 100644 --- a/src/util/redis_pool.rs +++ b/src/util/redis_pool.rs @@ -139,6 +139,20 @@ impl RedisPool { self.with_conn(a) } + pub fn lrem(&self, redis_key: &str, count: i64, value: T) + where + T: redis::ToRedisArgs, + { + let a = |conn: &redis::Connection| { + redis::cmd("lrem") + .arg(redis_key) + .arg(count) + .arg(value) + .execute(conn) + }; + self.with_conn(a) + } + pub fn lrange(&self, redis_key: &str, start: i64, stop: i64) -> T where T: redis::FromRedisValue, diff --git a/static/js/admin/article_list.js b/static/js/admin/article_list.js index e17caf0..21fff01 100644 --- a/static/js/admin/article_list.js +++ b/static/js/admin/article_list.js @@ -9,8 +9,8 @@ function getArticleList() { $("#next").attr({ "disabled": "disabled" }); } for (var index in result.data) { - result.data[index].create_time = moment.utc(result.data[index].create_time).local().format(); - result.data[index].modify_time = moment.utc(result.data[index].modify_time).local().format(); + result.data[index].create_time = moment.utc(result.data[index].create_time).local().format("YYYY-MM-DD HH:mm:ss"); + result.data[index].modify_time = moment.utc(result.data[index].modify_time).local().format("YYYY-MM-DD HH:mm:ss"); } var html = template("tpl-article-list", result); $("tbody").append(html); diff --git a/static/js/admin/ip_list.js b/static/js/admin/ip_list.js index 76a3918..b310b7e 100644 --- a/static/js/admin/ip_list.js +++ b/static/js/admin/ip_list.js @@ -11,7 +11,7 @@ function get_ip() { var ip_unique, ip_list = []; for(var i = 0; i < result.data.length; i++){ var data = JSON.parse(result.data[i]); - data.timestamp = moment.utc(data.timestamp).local().format(); + data.timestamp = moment.utc(data.timestamp).local().format("YYYY-MM-DD HH:mm:ss"); var html = template("tpl-ip", data); $("tbody").append(html); ip_list.push(data.ip) diff --git a/static/js/admin/user_list.js b/static/js/admin/user_list.js index 932602e..b462eb6 100644 --- a/static/js/admin/user_list.js +++ b/static/js/admin/user_list.js @@ -9,7 +9,7 @@ function getUserList() { $("#next").attr({ "disabled": "disabled" }); } for (var index in result.data) { - result.data[index].create_time = moment.utc(result.data[index].create_time).local().format(); + result.data[index].create_time = moment.utc(result.data[index].create_time).local().format("YYYY-MM-DD HH:mm:ss"); if (result.data[index].groups === 0) { result.data[index].group_name = "Admin" } else { diff --git a/views/admin/article_view.html b/views/admin/article_view.html index 7c29b60..e9043f9 100644 --- a/views/admin/article_view.html +++ b/views/admin/article_view.html @@ -37,7 +37,7 @@ $.getJSON("/api/v1/article/admin/view?id=" + id, function (result) { $(".col-md-offset-1") .append("
" + - "
"); var tags = {data: []}; diff --git a/views/admin/users.html b/views/admin/users.html index 5862b7e..97b8f73 100644 --- a/views/admin/users.html +++ b/views/admin/users.html @@ -10,7 +10,7 @@ {% endblock css %} {% block body %} -
+
@@ -19,6 +19,7 @@ + diff --git a/views/template/user_list.html b/views/template/user_list.html index 23037d3..6f8fc17 100644 --- a/views/template/user_list.html +++ b/views/template/user_list.html @@ -6,6 +6,7 @@ +
账号 昵称 权限Github Email 创建时间 操作

{{ $value.account }}

{{ $value.nickname }}

{{ $value.group_name }}

{{ $value.github }} {{ $value.email }} {{ $value.create_time }} diff --git a/views/visitor/user_info.html b/views/visitor/user_info.html index ffde16c..878d538 100644 --- a/views/visitor/user_info.html +++ b/views/visitor/user_info.html @@ -48,6 +48,7 @@

{{ user_info.nickname }}

{{ user_info.say }}

+
{{ user_info.email }}
From 425ff2aa7de589c710780a65158ff590023c8d78 Mon Sep 17 00:00:00 2001 From: piaoliu <441594700@qq.com> Date: Wed, 4 Apr 2018 01:26:44 +0800 Subject: [PATCH 2/3] 1. add notifys module 2. modify struct NewComment 3. modify redis pool function 4. modify user add comment api 5. add view admin account function 6. modify js to add new comment --- src/api/user_api.rs | 33 +++++++++++- src/lib.rs | 1 + src/models/articles.rs | 2 + src/models/comment.rs | 9 ++++ src/models/mod.rs | 2 + src/models/notifys.rs | 78 ++++++++++++++++++++++++++++ src/models/user.rs | 19 ++++++- src/util/redis_pool.rs | 14 ++++- src/web/visitor.rs | 23 ++++++-- static/js/article_view/wangeditor.js | 5 +- 10 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 src/models/notifys.rs diff --git a/src/api/user_api.rs b/src/api/user_api.rs index 0a98d98..970d650 100644 --- a/src/api/user_api.rs +++ b/src/api/user_api.rs @@ -4,7 +4,7 @@ use sapper_std::{JsonParams, SessionVal}; use serde_json; use super::super::{ChangePassword, DeleteComment, EditUser, LoginUser, NewComments, Permissions, - Postgresql, Redis, UserInfo}; + Postgresql, Redis, UserInfo, UserNotify, ArticlesWithTag}; pub struct User; @@ -64,10 +64,39 @@ impl User { } fn new_comment(req: &mut Request) -> SapperResult { - let body: NewComments = get_json_params!(req); + let mut body: NewComments = get_json_params!(req); let cookie = req.ext().get::().unwrap(); let redis_pool = req.ext().get::().unwrap(); let pg_pool = req.ext().get::().unwrap().get().unwrap(); + let user = + serde_json::from_str::(&UserInfo::view_user_with_cookie(redis_pool, cookie)).unwrap(); + let admin = UserInfo::view_admin(&pg_pool); + let article = ArticlesWithTag::query_without_article(&pg_pool, body.article_id(), false).unwrap(); + + if let Some(reply_user_id) = body.reply_user_id() { + if user.id != reply_user_id { + let user_reply_notify = UserNotify { + user_id: reply_user_id, + send_user_name: user.nickname.clone(), + article_id: article.id, + article_title: article.title.clone(), + notify_type: "reply".into(), + }; + user_reply_notify.cache(&redis_pool); + } + } + + if user.groups != 0 { + let comment_notify = UserNotify { + user_id: admin.id, + send_user_name: user.nickname.clone(), + article_id: article.id, + article_title: article.title.clone(), + notify_type: "comment".into(), + }; + comment_notify.cache(&redis_pool); + } + let res = json!({ "status": body.insert(&pg_pool, redis_pool, cookie) }); diff --git a/src/lib.rs b/src/lib.rs index c8829f9..ef52aa7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ pub(crate) use models::{ChangePassword, ChangePermission, DisabledUser, EditUser RegisteredUser, UserInfo, Users}; pub(crate) use models::{NewTag, TagCount, Tags}; pub(crate) use models::{Comments, DeleteComment, NewComments}; +pub(crate) use models::UserNotify; pub(crate) use util::{get_password, markdown_render, random_string, sha3_256_encode}; pub(crate) use util::{get_github_account_nickname_address, get_github_token, get_github_primary_email}; #[cfg(not(feature = "monitor"))] diff --git a/src/models/articles.rs b/src/models/articles.rs index 16e92aa..453c666 100644 --- a/src/models/articles.rs +++ b/src/models/articles.rs @@ -290,6 +290,7 @@ impl RawArticlesWithTag { fn into_without_content(self) -> ArticlesWithoutContent { ArticlesWithoutContent { id: self.id, + title: self.title, published: self.published, tags_id: self.tags_id, tags: self.tags, @@ -333,6 +334,7 @@ impl PublishedStatistics { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ArticlesWithoutContent { pub id: Uuid, + pub title: String, pub published: bool, pub tags_id: Vec>, pub tags: Vec>, diff --git a/src/models/comment.rs b/src/models/comment.rs index 56d01c1..0b4d9bf 100644 --- a/src/models/comment.rs +++ b/src/models/comment.rs @@ -70,6 +70,7 @@ impl InsertComments { pub struct NewComments { comment: String, article_id: Uuid, + reply_user_id: Option, } impl NewComments { @@ -86,6 +87,14 @@ impl NewComments { serde_json::from_str::(&redis_pool.hget::(cookie, "info")).unwrap(); self.into_insert_comments(info.id).insert(conn) } + + pub fn reply_user_id(&mut self) -> Option { + self.reply_user_id.take() + } + + pub fn article_id(&self) -> Uuid { + self.article_id + } } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/src/models/mod.rs b/src/models/mod.rs index 2c22820..b5b6b35 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -3,6 +3,7 @@ pub mod user; pub mod tag; pub mod article_tag_relation; pub mod comment; +pub mod notifys; pub(crate) use self::articles::{ArticleList, ArticlesWithTag, EditArticle, ModifyPublish, NewArticle}; @@ -12,3 +13,4 @@ pub(crate) use self::user::{ChangePassword, ChangePermission, DisabledUser, Edit pub(crate) use self::tag::{NewTag, TagCount, Tags}; pub(crate) use self::article_tag_relation::{RelationTag, Relations}; pub(crate) use self::comment::{Comments, DeleteComment, NewComments}; +pub(crate) use self::notifys::UserNotify; diff --git a/src/models/notifys.rs b/src/models/notifys.rs new file mode 100644 index 0000000..679bcb3 --- /dev/null +++ b/src/models/notifys.rs @@ -0,0 +1,78 @@ +use super::super::RedisPool; + +use serde_json; +use std::sync::Arc; +use uuid::Uuid; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct UserNotify { + pub user_id: Uuid, + pub send_user_name: String, + pub article_id: Uuid, + pub article_title: String, + pub notify_type: String, +} + +impl UserNotify { + /// Cache user's comment notify to redis + pub fn cache(&self, redis_pool: &Arc) { + let content = serde_json::to_string(self).unwrap(); + let notify_key = format!("notify:{}:{}", self.article_id.hyphenated().to_string(), self.user_id.hyphenated().to_string()); + // remove old value + redis_pool.lrem(¬ify_key, 0, &content); + // put new value to list top + redis_pool.lpush(¬ify_key, &content); + // set expire time 15 day or increase expire time to 15 day + const EXPIRE_TIME: i64 = 5 * 24 * 3600; + redis_pool.expire(¬ify_key, EXPIRE_TIME); + // limit list size to 100 + redis_pool.ltrim(¬ify_key, 0, 10); + } + + /// Get all the notifications about the user + pub fn get_notifys(user_id: Uuid, redis_pool: &Arc) -> Option> { + let pattern = format!("notify:*:{}", user_id.hyphenated().to_string()); + let mut notify = Vec::new(); + + for notify_key in redis_pool.keys(&pattern) { + let notifys: Vec = redis_pool.lrange(¬ify_key, 0, -1); + let notifys: Vec = notifys + .iter() + .map(|notify_string| { + let user_notify: UserNotify = serde_json::from_str(¬ify_string).unwrap(); + user_notify + }) + .collect(); + notify.extend(notifys); + + } + + if notify.is_empty() { + Some(notify) + } else { + None + } + } + + /// Remove the notification of the specified article specified user + pub fn remove_notifys_with_article_and_user( + user_id: Uuid, + article_id: Uuid, + redis_pool: &Arc, + ) { + let notify_key = format!("notify:{}:{}", article_id.hyphenated().to_string(), user_id.hyphenated().to_string()); + redis_pool.del(notify_key); + } + + /// Remove the notification of the specified article, e.g use on remove the specified article + pub fn remove_with_article(article_id: Uuid, redis_pool: &Arc) { + let pattern = format!("notify:{}*", article_id.hyphenated().to_string()); + redis_pool.del(redis_pool.keys(&pattern)); + } + + /// Remove the notification of the user, e.g use on remove the user + pub fn remove_with_user(user_id: Uuid, redis_pool: &Arc) { + let pattern = format!("notify:*:{}", user_id.hyphenated().to_string()); + redis_pool.del(redis_pool.keys(&pattern)); + } +} diff --git a/src/models/user.rs b/src/models/user.rs index ec47b5c..108b4e9 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -177,6 +177,7 @@ impl UserInfo { Err(err) => Err(format!("{}", err)), } } + pub fn view_user_with_cookie(redis_pool: &Arc, cookie: &str) -> String { redis_pool.hget::(cookie, "info") } @@ -206,6 +207,22 @@ impl UserInfo { Err(err) => Err(format!("{}", err)), } } + + pub fn view_admin(conn: &PgConnection) -> Self { + all_users + .select(( + users::id, + users::account, + users::nickname, + users::groups, + users::say, + users::email, + users::create_time, + users::github, + )) + .filter(users::account.eq("admin")) + .get_result::(conn).unwrap() + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -333,7 +350,7 @@ impl LoginUser { } pub fn sign_out(redis_pool: &Arc, cookies: &str) -> bool { - redis_pool.del(&cookies) + redis_pool.del(cookies) } pub fn login_with_github( diff --git a/src/util/redis_pool.rs b/src/util/redis_pool.rs index 8423adc..0917ce6 100644 --- a/src/util/redis_pool.rs +++ b/src/util/redis_pool.rs @@ -43,6 +43,13 @@ impl RedisPool { } } + pub fn keys(&self, pattern: &str) -> Vec { + redis::cmd("keys") + .arg(pattern) + .query(&*self.pool.get().unwrap()) + .unwrap() + } + pub fn exists(&self, redis_key: &str) -> bool { redis::cmd("exists") .arg(redis_key) @@ -56,9 +63,12 @@ impl RedisPool { self.with_conn(a); } - pub fn del(&self, redis_key: &str) -> bool { + pub fn del(&self, redis_keys: T) -> bool + where + T: redis::ToRedisArgs, + { redis::cmd("del") - .arg(redis_key) + .arg(redis_keys) .query(&*self.pool.get().unwrap()) .unwrap() } diff --git a/src/web/visitor.rs b/src/web/visitor.rs index e276362..4cc5c9d 100644 --- a/src/web/visitor.rs +++ b/src/web/visitor.rs @@ -1,10 +1,11 @@ use sapper::{Request, Response, Result as SapperResult, SapperModule, SapperRouter}; -use sapper_std::{render, PathParams}; +use sapper_std::{render, PathParams, SessionVal}; use uuid::Uuid; +use serde_json; -use super::super::{ArticlesWithTag, Permissions, Postgresql, TagCount, UserInfo, WebContext}; +use super::super::{ArticlesWithTag, Permissions, Postgresql, TagCount, UserInfo, WebContext, UserNotify, Redis}; #[cfg(not(feature = "monitor"))] -use super::super::{visitor_log, Redis}; +use super::super::visitor_log; pub struct ArticleWeb; @@ -61,10 +62,24 @@ impl ArticleWeb { let params = get_path_params!(req); let article_id: Uuid = t_param!(params, "id").clone().parse().unwrap(); let pg_pool = req.ext().get::().unwrap().get().unwrap(); + let redis_pool = req.ext().get::().unwrap(); let mut web = req.ext().get::().unwrap().clone(); match ArticlesWithTag::query_article(&pg_pool, article_id, false) { - Ok(ref data) => web.add("article", data), + Ok(ref data) => { + web.add("article", data); + + // remove user's notify about this article + req.ext().get::().and_then(| cookie| { + if redis_pool.exists(cookie) { + let info = serde_json::from_str::(&redis_pool + .hget::(cookie, "info")) + .unwrap(); + UserNotify::remove_notifys_with_article_and_user(info.id, data.id, &redis_pool); + }; + Some(()) + }); + }, Err(err) => println!("{}", err), } res_html!("visitor/article_view.html", web) diff --git a/static/js/article_view/wangeditor.js b/static/js/article_view/wangeditor.js index 087e057..d58a993 100644 --- a/static/js/article_view/wangeditor.js +++ b/static/js/article_view/wangeditor.js @@ -6,11 +6,14 @@ $("button.comment").click(function () { if (editor.txt.text() !== "") { var comment = editor.txt.html(); var article_id = $(".col-md-offset-1[data-id]").attr("data-id"); + if ($(".w-e-text .post-meta a").length > 0) { + var reply_user_id = $(".w-e-text .post-meta a").attr("href").split("/")[2]; + } $.ajax({ url: "/api/v1/comment/new", type: "post", dataType: "json", - data: JSON.stringify({ "comment": comment, "article_id": article_id }), + data: JSON.stringify({ "comment": comment, "article_id": article_id, "reply_user_id": reply_user_id }), headers: { "Content-Type": "application/json" }, success: function (res) { if (res.status) { From 148004dd76230c52bc259fdb8f0d15e45664c722 Mon Sep 17 00:00:00 2001 From: piaoliu <441594700@qq.com> Date: Thu, 5 Apr 2018 01:35:20 +0800 Subject: [PATCH 3/3] Upgrade to 0.2.0 --- Cargo.toml | 2 +- src/api/admin_article_api.rs | 5 +- src/api/admin_user_api.rs | 5 +- src/api/user_api.rs | 56 ++++++---- src/api/visitor_api.rs | 14 ++- src/lib.rs | 9 +- src/models/article_tag_relation.rs | 3 +- src/models/articles.rs | 24 +++-- src/models/comment.rs | 6 +- src/models/notifys.rs | 17 ++- src/models/tag.rs | 9 +- src/models/user.rs | 137 ++++++++++++++----------- src/util/github_information.rs | 130 ++++++++++++----------- src/util/mod.rs | 10 +- src/util/redis_pool.rs | 17 ++- src/web/admin.rs | 7 ++ src/web/visitor.rs | 16 +-- static/js/article_view/article_view.js | 1 + views/admin/admin_base.html | 3 + views/admin/ip.html | 2 +- views/admin/notify.html | 38 +++++++ views/visitor/base.html | 18 ++++ 22 files changed, 347 insertions(+), 182 deletions(-) create mode 100644 views/admin/notify.html diff --git a/Cargo.toml b/Cargo.toml index f4aa1ca..cd0ece2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "blog" -version = "0.1.0" +version = "0.2.0" authors = ["piaoliu <441594700@qq.com>"] [dependencies] diff --git a/src/api/admin_article_api.rs b/src/api/admin_article_api.rs index 831982b..2bc5671 100644 --- a/src/api/admin_article_api.rs +++ b/src/api/admin_article_api.rs @@ -5,7 +5,7 @@ use serde_json; use uuid::Uuid; use super::super::{ArticleList, ArticlesWithTag, EditArticle, ModifyPublish, NewArticle, - Permissions, Postgresql}; + Permissions, Postgresql, Redis}; pub struct AdminArticle; @@ -25,8 +25,9 @@ impl AdminArticle { let params = get_path_params!(req); let article_id: Uuid = t_param!(params, "id").clone().parse().unwrap(); let pg_pool = req.ext().get::().unwrap().get().unwrap(); + let redis_pool = req.ext().get::().unwrap(); - let res = match ArticlesWithTag::delete_with_id(&pg_pool, article_id) { + let res = match ArticlesWithTag::delete_with_id(&pg_pool, redis_pool, article_id) { Ok(num_deleted) => json!({ "status": true, "num_deleted": num_deleted diff --git a/src/api/admin_user_api.rs b/src/api/admin_user_api.rs index 604beb1..6abe608 100644 --- a/src/api/admin_user_api.rs +++ b/src/api/admin_user_api.rs @@ -4,7 +4,7 @@ use serde_json; use sapper_std::{JsonParams, PathParams, QueryParams}; use uuid::Uuid; -use super::super::{ChangePermission, DisabledUser, Permissions, Postgresql, UserInfo, Users}; +use super::super::{ChangePermission, DisabledUser, Permissions, Postgresql, Redis, UserInfo, Users}; pub struct AdminUser; @@ -13,8 +13,9 @@ impl AdminUser { let params = get_path_params!(req); let user_id: Uuid = t_param!(params, "id").clone().parse().unwrap(); let pg_pool = req.ext().get::().unwrap().get().unwrap(); + let redis_pool = req.ext().get::().unwrap(); - let res = match Users::delete(&pg_pool, user_id) { + let res = match Users::delete(&pg_pool, redis_pool, user_id) { Ok(num_deleted) => json!({ "status": true, "num_deleted": num_deleted diff --git a/src/api/user_api.rs b/src/api/user_api.rs index 970d650..0c8d775 100644 --- a/src/api/user_api.rs +++ b/src/api/user_api.rs @@ -3,8 +3,8 @@ use sapper::{Error as SapperError, Request, Response, Result as SapperResult, Sa use sapper_std::{JsonParams, SessionVal}; use serde_json; -use super::super::{ChangePassword, DeleteComment, EditUser, LoginUser, NewComments, Permissions, - Postgresql, Redis, UserInfo, UserNotify, ArticlesWithTag}; +use super::super::{ArticlesWithTag, ChangePassword, DeleteComment, EditUser, LoginUser, + NewComments, Permissions, Postgresql, Redis, UserInfo, UserNotify}; pub struct User; @@ -69,12 +69,16 @@ impl User { let redis_pool = req.ext().get::().unwrap(); let pg_pool = req.ext().get::().unwrap().get().unwrap(); let user = - serde_json::from_str::(&UserInfo::view_user_with_cookie(redis_pool, cookie)).unwrap(); - let admin = UserInfo::view_admin(&pg_pool); - let article = ArticlesWithTag::query_without_article(&pg_pool, body.article_id(), false).unwrap(); - - if let Some(reply_user_id) = body.reply_user_id() { - if user.id != reply_user_id { + serde_json::from_str::(&UserInfo::view_user_with_cookie(redis_pool, cookie)) + .unwrap(); + let admin = UserInfo::view_admin(&pg_pool, redis_pool); + let article = + ArticlesWithTag::query_without_article(&pg_pool, body.article_id(), false).unwrap(); + + match body.reply_user_id() { + // Reply comment + Some(reply_user_id) => { + // Notification replyee let user_reply_notify = UserNotify { user_id: reply_user_id, send_user_name: user.nickname.clone(), @@ -83,18 +87,32 @@ impl User { notify_type: "reply".into(), }; user_reply_notify.cache(&redis_pool); - } - } - if user.groups != 0 { - let comment_notify = UserNotify { - user_id: admin.id, - send_user_name: user.nickname.clone(), - article_id: article.id, - article_title: article.title.clone(), - notify_type: "comment".into(), - }; - comment_notify.cache(&redis_pool); + // If the sender is not an admin and also the responder is also not admin, notify admin + if reply_user_id != admin.id && user.groups != 0 { + let comment_notify = UserNotify { + user_id: admin.id, + send_user_name: user.nickname.clone(), + article_id: article.id, + article_title: article.title.clone(), + notify_type: "comment".into(), + }; + comment_notify.cache(&redis_pool); + } + } + // Normal comment + None => { + if user.groups != 0 { + let comment_notify = UserNotify { + user_id: admin.id, + send_user_name: user.nickname.clone(), + article_id: article.id, + article_title: article.title.clone(), + notify_type: "comment".into(), + }; + comment_notify.cache(&redis_pool); + } + } } let res = json!({ diff --git a/src/api/visitor_api.rs b/src/api/visitor_api.rs index 0867596..b51cbe5 100644 --- a/src/api/visitor_api.rs +++ b/src/api/visitor_api.rs @@ -4,8 +4,9 @@ use sapper::header::{ContentType, Location}; use sapper::status; use serde_json; -use super::super::{ArticleList, ArticlesWithTag, Comments, LoginUser, Permissions, Postgresql, - Redis, RegisteredUser, UserInfo, get_github_token, get_github_account_nickname_address}; +use super::super::{get_github_account_nickname_address, get_github_token, ArticleList, + ArticlesWithTag, Comments, LoginUser, Permissions, Postgresql, Redis, + RegisteredUser, UserInfo}; use uuid::Uuid; pub struct Visitor; @@ -167,7 +168,14 @@ impl Visitor { response.headers_mut().set(ContentType::json()); let (account, nickname, github_address) = get_github_account_nickname_address(&token)?; - match LoginUser::login_with_github(&pg_pool, redis_pool, github_address, nickname, account, &token) { + match LoginUser::login_with_github( + &pg_pool, + redis_pool, + github_address, + nickname, + account, + &token, + ) { Ok(cookie) => { let res = json!({ "status": true, diff --git a/src/lib.rs b/src/lib.rs index ef52aa7..c3020eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,8 @@ extern crate diesel; #[macro_use] extern crate diesel_infer_schema; extern crate dotenv; +extern crate hyper; +extern crate hyper_native_tls; extern crate r2d2; extern crate r2d2_redis; extern crate rand; @@ -22,11 +24,9 @@ extern crate serde; extern crate serde_derive; #[macro_use] extern crate serde_json; +extern crate serde_urlencoded; extern crate tiny_keccak; extern crate uuid; -extern crate hyper; -extern crate hyper_native_tls; -extern crate serde_urlencoded; pub mod schema; pub mod models; @@ -43,7 +43,8 @@ pub(crate) use models::{NewTag, TagCount, Tags}; pub(crate) use models::{Comments, DeleteComment, NewComments}; pub(crate) use models::UserNotify; pub(crate) use util::{get_password, markdown_render, random_string, sha3_256_encode}; -pub(crate) use util::{get_github_account_nickname_address, get_github_token, get_github_primary_email}; +pub(crate) use util::{get_github_account_nickname_address, get_github_primary_email, + get_github_token}; #[cfg(not(feature = "monitor"))] pub(crate) use util::visitor_log; pub use util::{create_redis_pool, get_identity_and_web_context, Permissions, Redis, RedisPool, diff --git a/src/models/article_tag_relation.rs b/src/models/article_tag_relation.rs index 75b72d4..317d7bb 100644 --- a/src/models/article_tag_relation.rs +++ b/src/models/article_tag_relation.rs @@ -64,7 +64,8 @@ impl RelationTag { } pub fn insert_all(&self, conn: &PgConnection) -> bool { - // If `tag` exist, insert all the new tags into the table all at once, and return the ID of the newly added tag + // If `tag` exist, insert all the new tags into the table all at once, + // and return the ID of the newly added tag let mut tags_id = if self.tag.is_some() { NewTag::insert_all( self.tag diff --git a/src/models/articles.rs b/src/models/articles.rs index 453c666..787e83c 100644 --- a/src/models/articles.rs +++ b/src/models/articles.rs @@ -1,14 +1,15 @@ use super::super::articles::dsl::articles as all_articles; use super::super::{article_with_tag, articles}; use super::super::article_with_tag::dsl::article_with_tag as all_article_with_tag; -use super::super::markdown_render; -use super::{RelationTag, Relations}; +use super::super::{markdown_render, RedisPool}; +use super::{RelationTag, Relations, UserNotify}; use chrono::NaiveDateTime; use diesel; use diesel::prelude::*; use diesel::sql_types::{BigInt, Text}; use uuid::Uuid; +use std::sync::Arc; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ArticlesWithTag { @@ -23,11 +24,18 @@ pub struct ArticlesWithTag { } impl ArticlesWithTag { - pub fn delete_with_id(conn: &PgConnection, id: Uuid) -> Result { + pub fn delete_with_id( + conn: &PgConnection, + redis_pool: &Arc, + id: Uuid, + ) -> Result { Relations::delete_all(conn, id, "article"); - let res = diesel::delete(all_articles.filter(articles::id.eq(id))).execute(conn); + let res = diesel::delete(all_articles.filter(articles::id.eq(&id))).execute(conn); match res { - Ok(data) => Ok(data), + Ok(data) => { + UserNotify::remove_with_article(id, redis_pool); + Ok(data) + } Err(err) => Err(format!("{}", err)), } } @@ -314,8 +322,10 @@ struct Articles { #[derive(Queryable, Debug, Clone, Deserialize, Serialize, QueryableByName)] #[table_name = "articles"] pub struct PublishedStatistics { - #[sql_type = "Text"] pub dimension: String, - #[sql_type = "BigInt"] pub quantity: i64, + #[sql_type = "Text"] + pub dimension: String, + #[sql_type = "BigInt"] + pub quantity: i64, } impl PublishedStatistics { diff --git a/src/models/comment.rs b/src/models/comment.rs index 0b4d9bf..3ee8fbf 100644 --- a/src/models/comment.rs +++ b/src/models/comment.rs @@ -17,7 +17,8 @@ pub struct Comments { comment: String, article_id: Uuid, user_id: Uuid, - #[sql_type = "Text"] nickname: String, + #[sql_type = "Text"] + nickname: String, create_time: NaiveDateTime, } @@ -114,8 +115,7 @@ impl DeleteComment { match *permission { Some(0) => Comments::delete_with_comment_id(conn, self.comment_id), _ => { - let info = serde_json::from_str::(&redis_pool - .hget::(cookie, "info")) + let info = serde_json::from_str::(&redis_pool.hget::(cookie, "info")) .unwrap(); if self.user_id == info.id { Comments::delete_with_comment_id(conn, self.comment_id) diff --git a/src/models/notifys.rs b/src/models/notifys.rs index 679bcb3..e2d9baf 100644 --- a/src/models/notifys.rs +++ b/src/models/notifys.rs @@ -17,7 +17,11 @@ impl UserNotify { /// Cache user's comment notify to redis pub fn cache(&self, redis_pool: &Arc) { let content = serde_json::to_string(self).unwrap(); - let notify_key = format!("notify:{}:{}", self.article_id.hyphenated().to_string(), self.user_id.hyphenated().to_string()); + let notify_key = format!( + "notify:{}:{}", + self.article_id.hyphenated().to_string(), + self.user_id.hyphenated().to_string() + ); // remove old value redis_pool.lrem(¬ify_key, 0, &content); // put new value to list top @@ -44,13 +48,12 @@ impl UserNotify { }) .collect(); notify.extend(notifys); - } if notify.is_empty() { - Some(notify) - } else { None + } else { + Some(notify) } } @@ -60,7 +63,11 @@ impl UserNotify { article_id: Uuid, redis_pool: &Arc, ) { - let notify_key = format!("notify:{}:{}", article_id.hyphenated().to_string(), user_id.hyphenated().to_string()); + let notify_key = format!( + "notify:{}:{}", + article_id.hyphenated().to_string(), + user_id.hyphenated().to_string() + ); redis_pool.del(notify_key); } diff --git a/src/models/tag.rs b/src/models/tag.rs index 04ea878..0f96145 100644 --- a/src/models/tag.rs +++ b/src/models/tag.rs @@ -49,9 +49,12 @@ impl Tags { #[derive(Queryable, Debug, Clone, Deserialize, Serialize, QueryableByName)] #[table_name = "article_tag_relation"] pub struct TagCount { - #[sql_type = "sql_uuid"] id: Uuid, - #[sql_type = "Text"] tag: String, - #[sql_type = "BigInt"] count: i64, + #[sql_type = "sql_uuid"] + id: Uuid, + #[sql_type = "Text"] + tag: String, + #[sql_type = "BigInt"] + count: i64, } impl TagCount { diff --git a/src/models/user.rs b/src/models/user.rs index 108b4e9..fbc8a1d 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -7,8 +7,10 @@ use diesel::prelude::*; use uuid::Uuid; use serde_json; use std::sync::Arc; +use super::UserNotify; -use super::super::{get_password, random_string, RedisPool, sha3_256_encode, get_github_primary_email}; +use super::super::{get_github_primary_email, get_password, random_string, RedisPool, + sha3_256_encode}; #[derive(Queryable, Debug, Clone, Deserialize, Serialize)] pub struct Users { @@ -26,10 +28,17 @@ pub struct Users { } impl Users { - pub fn delete(conn: &PgConnection, id: Uuid) -> Result { + pub fn delete( + conn: &PgConnection, + redis_pool: &Arc, + id: Uuid, + ) -> Result { let res = diesel::delete(all_users.find(id)).execute(conn); match res { - Ok(data) => Ok(data), + Ok(data) => { + UserNotify::remove_with_user(id, redis_pool); + Ok(data) + } Err(err) => Err(format!("{}", err)), } } @@ -208,20 +217,29 @@ impl UserInfo { } } - pub fn view_admin(conn: &PgConnection) -> Self { - all_users - .select(( - users::id, - users::account, - users::nickname, - users::groups, - users::say, - users::email, - users::create_time, - users::github, - )) - .filter(users::account.eq("admin")) - .get_result::(conn).unwrap() + /// Get admin information, cache on redis + /// key is `admin_info` + pub fn view_admin(conn: &PgConnection, redis_pool: &Arc) -> Self { + if redis_pool.exists("admin_info") { + serde_json::from_str::(&redis_pool.get("admin_info")).unwrap() + } else { + let info = all_users + .select(( + users::id, + users::account, + users::nickname, + users::groups, + users::say, + users::email, + users::create_time, + users::github, + )) + .filter(users::account.eq("admin")) + .get_result::(conn) + .unwrap(); + redis_pool.set("admin_info", &json!(&info).to_string()); + info + } } } @@ -366,53 +384,52 @@ impl LoginUser { .filter(users::disabled.eq(0)) .filter(users::github.eq(&github)) .get_result::(conn) - { - // github already exists - Ok(data) => { - let cookie = sha3_256_encode(random_string(8)); - redis_pool.hset(&cookie, "login_time", Local::now().timestamp()); - redis_pool.hset(&cookie, "info", json!(data.into_user_info()).to_string()); - redis_pool.expire(&cookie, ttl); - Ok(cookie) - } - Err(_) => { - let email = match get_github_primary_email(token) { - Ok(data) => data, - Err(e) => return Err(e), - }; - - match all_users - .filter(users::disabled.eq(0)) - .filter(users::email.eq(&email)) - .get_result::(conn) - { - // Account already exists but not linked - Ok(data) => { - let res = diesel::update(all_users.filter(users::id.eq(data.id))) - .set(users::github.eq(github)) - .get_result::(conn); - match res { - Ok(info) => { - let cookie = sha3_256_encode(random_string(8)); - redis_pool.hset(&cookie, "login_time", Local::now().timestamp()); - redis_pool.hset( - &cookie, - "info", - json!(info.into_user_info()).to_string(), - ); - redis_pool.expire(&cookie, ttl); - Ok(cookie) - } - Err(err) => Err(format!("{}", err)), - } - } - // sign up - Err(_) => { - NewUser::new_with_github(email, github, account,nickname).insert(conn, redis_pool) + { + // github already exists + Ok(data) => { + let cookie = sha3_256_encode(random_string(8)); + redis_pool.hset(&cookie, "login_time", Local::now().timestamp()); + redis_pool.hset(&cookie, "info", json!(data.into_user_info()).to_string()); + redis_pool.expire(&cookie, ttl); + Ok(cookie) + } + Err(_) => { + let email = match get_github_primary_email(token) { + Ok(data) => data, + Err(e) => return Err(e), + }; + + match all_users + .filter(users::disabled.eq(0)) + .filter(users::email.eq(&email)) + .get_result::(conn) + { + // Account already exists but not linked + Ok(data) => { + let res = diesel::update(all_users.filter(users::id.eq(data.id))) + .set(users::github.eq(github)) + .get_result::(conn); + match res { + Ok(info) => { + let cookie = sha3_256_encode(random_string(8)); + redis_pool.hset(&cookie, "login_time", Local::now().timestamp()); + redis_pool.hset( + &cookie, + "info", + json!(info.into_user_info()).to_string(), + ); + redis_pool.expire(&cookie, ttl); + Ok(cookie) } + Err(err) => Err(format!("{}", err)), } + } + // sign up + Err(_) => NewUser::new_with_github(email, github, account, nickname) + .insert(conn, redis_pool), } } + } } } diff --git a/src/util/github_information.rs b/src/util/github_information.rs index 987adeb..026479a 100644 --- a/src/util/github_information.rs +++ b/src/util/github_information.rs @@ -16,37 +16,40 @@ fn create_https_client() -> Client { pub fn get_github_token(code: &str) -> Result { let client = create_https_client(); - let params = serde_urlencoded::to_string( - [ - ("client_id", "52b10cd3fff369999cd9"), - ("client_secret", "212d9729ead001da8844c1dcc79d45240166bd4f"), - ("code", code), - ("accept", "json"), - ], - ).unwrap(); + let params = serde_urlencoded::to_string([ + ("client_id", "52b10cd3fff369999cd9"), + ("client_secret", "212d9729ead001da8844c1dcc79d45240166bd4f"), + ("code", code), + ("accept", "json"), + ]).unwrap(); - client.post("https://github.com/login/oauth/access_token") + client + .post("https://github.com/login/oauth/access_token") .header(ContentType::form_url_encoded()) .body(¶ms) .send() .map_err(|e| SapperError::Custom(format!("hyper's io error: '{}'", e))) .and_then(|mut response| { let mut body = String::new(); - response.read_to_string(&mut body) + response + .read_to_string(&mut body) .map_err(|e| SapperError::Custom(format!("read body error: '{}'", e))) .map(|_| body) - }).and_then(|ref body| { - #[derive(Deserialize)] - struct Inner { - access_token: String - } - serde_urlencoded::from_str::(body) - .map_err(|_| SapperError::Custom(String::from("No permission"))) - .map(|inner| inner.access_token) - }) + }) + .and_then(|ref body| { + #[derive(Deserialize)] + struct Inner { + access_token: String, + } + serde_urlencoded::from_str::(body) + .map_err(|_| SapperError::Custom(String::from("No permission"))) + .map(|inner| inner.access_token) + }) } -pub fn get_github_account_nickname_address(raw_token: &str) -> Result<(String ,String, String), SapperError> { +pub fn get_github_account_nickname_address( + raw_token: &str, +) -> Result<(String, String, String), SapperError> { let client = create_https_client(); let token = serde_urlencoded::to_string([("access_token", raw_token)]).unwrap(); @@ -55,34 +58,41 @@ pub fn get_github_account_nickname_address(raw_token: &str) -> Result<(String ,S let mut header = Headers::new(); header.append_raw("User-Agent", b"rustcc".to_vec()); - client.get(&user_url) + client + .get(&user_url) .headers(header) .send() .map_err(|e| SapperError::Custom(format!("hyper's io error: '{}'", e))) - .and_then(|mut response|{ + .and_then(|mut response| { let mut body = String::new(); - response.read_to_string(&mut body) + response + .read_to_string(&mut body) .map_err(|e| SapperError::Custom(format!("read body error: '{}'", e))) .map(|_| body) - }).and_then(|ref body| { - serde_json::from_str::(body) - .map_err(|e| SapperError::Custom(format!("read body error: '{}'", e))) - .and_then(|inner| { - let nickname = match inner["name"].as_str() { - Some(data) => data.to_string(), - None => return Err(SapperError::Custom(format!("read body error"))) - }; - let github_address = match inner["html_url"].as_str() { - Some(data) => data.to_string(), - None => return Err(SapperError::Custom(format!("read body error"))) - }; - let account = match inner["login"].as_str() { - Some(data) => data.to_string(), - None => return Err(SapperError::Custom(format!("read body error"))) - }; - Ok((account, nickname, github_address)) - }) - }) + }) + .and_then(|ref body| { + serde_json::from_str::(body) + .map_err(|e| SapperError::Custom(format!("read body error: '{}'", e))) + .and_then(|inner| { + let nickname = match inner["name"].as_str() { + Some(data) => data.to_string(), + None => { + return Err(SapperError::Custom(format!( + "Your github account is missing a nickname setting" + ))) + } + }; + let github_address = match inner["html_url"].as_str() { + Some(data) => data.to_string(), + None => return Err(SapperError::Custom(format!("read body error"))), + }; + let account = match inner["login"].as_str() { + Some(data) => data.to_string(), + None => return Err(SapperError::Custom(format!("read body error"))), + }; + Ok((account, nickname, github_address)) + }) + }) } pub fn get_github_primary_email(raw_token: &str) -> Result { @@ -93,27 +103,29 @@ pub fn get_github_primary_email(raw_token: &str) -> Result { let mut header = Headers::new(); header.append_raw("User-Agent", b"rustcc".to_vec()); - client.get(&email_url) + client + .get(&email_url) .headers(header) .send() .map_err(|e| format!("hyper's io error: '{}'", e)) - .and_then(|mut response|{ + .and_then(|mut response| { let mut body = String::new(); - response.read_to_string(&mut body) + response + .read_to_string(&mut body) .map_err(|e| format!("read body error: '{}'", e)) .map(|_| body) - }).and_then(|ref body| { - serde_json::from_str::>(body) - .map_err(|e| format!("read body error: '{}'", e)) - .map(|raw_emails| { - let primary_email = raw_emails - .iter() - .into_iter() - .filter(|x| x["primary"].as_bool().unwrap()) - .map(|x| x["email"].as_str().unwrap()) - .collect::>() - [0]; - primary_email.to_string() - }) - }) + }) + .and_then(|ref body| { + serde_json::from_str::>(body) + .map_err(|e| format!("read body error: '{}'", e)) + .map(|raw_emails| { + let primary_email = raw_emails + .iter() + .into_iter() + .filter(|x| x["primary"].as_bool().unwrap()) + .map(|x| x["email"].as_str().unwrap()) + .collect::>()[0]; + primary_email.to_string() + }) + }) } diff --git a/src/util/mod.rs b/src/util/mod.rs index 71fe9ba..b253f8f 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -4,7 +4,8 @@ pub mod github_information; pub use self::redis_pool::{create_redis_pool, Redis, RedisPool}; pub use self::postgresql_pool::{create_pg_pool, Postgresql}; -pub use self::github_information::{get_github_primary_email, get_github_token, get_github_account_nickname_address}; +pub use self::github_information::{get_github_account_nickname_address, get_github_primary_email, + get_github_token}; use rand::{thread_rng, Rng}; use tiny_keccak::Keccak; @@ -15,7 +16,7 @@ use sapper::{Key, Request}; use chrono::Utc; use ammonia::clean; use sapper_std::{Context, SessionVal}; -use super::UserInfo; +use super::{UserInfo, UserNotify}; use serde_json; /// Get random value @@ -68,10 +69,11 @@ pub fn get_identity_and_web_context(req: &Request) -> (Option, Context) { match cookie { Some(cookie) => { if redis_pool.exists(cookie) { - let info = serde_json::from_str::(&redis_pool - .hget::(cookie, "info")) + let info = serde_json::from_str::(&redis_pool.hget::(cookie, "info")) .unwrap(); + let notifys = UserNotify::get_notifys(info.id, redis_pool); web.add("user", &info); + web.add("notifys", ¬ifys); (Some(info.groups), web) } else { (None, web) diff --git a/src/util/redis_pool.rs b/src/util/redis_pool.rs index 0917ce6..67c61bc 100644 --- a/src/util/redis_pool.rs +++ b/src/util/redis_pool.rs @@ -73,6 +73,19 @@ impl RedisPool { .unwrap() } + pub fn set(&self, redis_key: &str, value: &str) { + let a = + |conn: &redis::Connection| redis::cmd("set").arg(redis_key).arg(value).execute(conn); + self.with_conn(a); + } + + pub fn get(&self, redis_key: &str) -> String { + redis::cmd("get") + .arg(redis_key) + .query(&*self.pool.get().unwrap()) + .unwrap() + } + pub fn hset(&self, redis_key: &str, hash_key: &str, value: T) where T: redis::ToRedisArgs, @@ -150,8 +163,8 @@ impl RedisPool { } pub fn lrem(&self, redis_key: &str, count: i64, value: T) - where - T: redis::ToRedisArgs, + where + T: redis::ToRedisArgs, { let a = |conn: &redis::Connection| { redis::cmd("lrem") diff --git a/src/web/admin.rs b/src/web/admin.rs index 491c78d..dacfb63 100644 --- a/src/web/admin.rs +++ b/src/web/admin.rs @@ -68,6 +68,11 @@ impl Admin { let web = req.ext().get::().unwrap().clone(); res_html!("admin/ip.html", web) } + + fn notify(req: &mut Request) -> SapperResult { + let web = req.ext().get::().unwrap().clone(); + res_html!("admin/notify.html", web) + } } impl SapperModule for Admin { @@ -97,6 +102,8 @@ impl SapperModule for Admin { router.get("/admin/ip", Admin::visitor_ip_log); + router.get("/admin/notify", Admin::notify); + Ok(()) } } diff --git a/src/web/visitor.rs b/src/web/visitor.rs index 4cc5c9d..805a335 100644 --- a/src/web/visitor.rs +++ b/src/web/visitor.rs @@ -3,7 +3,8 @@ use sapper_std::{render, PathParams, SessionVal}; use uuid::Uuid; use serde_json; -use super::super::{ArticlesWithTag, Permissions, Postgresql, TagCount, UserInfo, WebContext, UserNotify, Redis}; +use super::super::{ArticlesWithTag, Permissions, Postgresql, Redis, TagCount, UserInfo, + UserNotify, WebContext}; #[cfg(not(feature = "monitor"))] use super::super::visitor_log; @@ -70,16 +71,19 @@ impl ArticleWeb { web.add("article", data); // remove user's notify about this article - req.ext().get::().and_then(| cookie| { + req.ext().get::().and_then(|cookie| { if redis_pool.exists(cookie) { - let info = serde_json::from_str::(&redis_pool - .hget::(cookie, "info")) + let info = serde_json::from_str::(&redis_pool.hget::(cookie, "info")) .unwrap(); - UserNotify::remove_notifys_with_article_and_user(info.id, data.id, &redis_pool); + UserNotify::remove_notifys_with_article_and_user( + info.id, + data.id, + &redis_pool, + ); }; Some(()) }); - }, + } Err(err) => println!("{}", err), } res_html!("visitor/article_view.html", web) diff --git a/static/js/article_view/article_view.js b/static/js/article_view/article_view.js index 938096e..36a7836 100644 --- a/static/js/article_view/article_view.js +++ b/static/js/article_view/article_view.js @@ -49,6 +49,7 @@ $("body").on("click", "ul.comment li a.reply", function () { data.re_user_name = re_user.text(); data.re_user_url = re_user.attr('href'); var html = template("tpl-reply", data); + $(".w-e-text").focus(); editor.txt.html(html) }); diff --git a/views/admin/admin_base.html b/views/admin/admin_base.html index 77bea36..ff00c1c 100644 --- a/views/admin/admin_base.html +++ b/views/admin/admin_base.html @@ -67,6 +67,9 @@
  • 访问记录
  • +
  • + 通知 +