├── .github └── workflows │ └── ubuntu.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── diesel.toml ├── migrations ├── 00000000000000_diesel_initial_setup │ ├── down.sql │ └── up.sql └── 2020-10-19-161234_ini │ ├── down.sql │ └── up.sql ├── rustfmt.toml ├── src ├── api │ ├── auth.rs │ ├── blog.rs │ ├── item.rs │ └── mod.rs ├── bin │ ├── job.rs │ ├── serve.rs │ └── worker.rs ├── bot │ ├── cfg.rs │ ├── jobs.rs │ ├── mod.rs │ ├── spider.rs │ └── tasks.rs ├── db.rs ├── errors.rs ├── lib.rs ├── schema.rs ├── util │ ├── email.rs │ ├── helper.rs │ └── mod.rs └── view │ ├── form.rs │ ├── mod.rs │ └── tmpl.rs ├── static ├── 0_auth.js ├── 0_blog.js ├── 0_item.js ├── 0_submit.js ├── 0_tagbar.js ├── base64.js ├── favicon.ico ├── logo │ ├── Angular.svg │ ├── CPP.svg │ ├── Dart.svg │ ├── Go.svg │ ├── Java.svg │ ├── JavaScript.svg │ ├── Kotlin.svg │ ├── PHP.svg │ ├── Python.svg │ ├── React.svg │ ├── Rust.svg │ ├── Swift.svg │ ├── TypeScript.svg │ ├── Vue.svg │ ├── all.svg │ └── from.svg ├── main.js ├── profile.js └── styles.css ├── task.sh └── templates ├── 0_blog_form.html ├── 0_item_form.html ├── 0_submit_form.html ├── _auth_box.html ├── _banner.html ├── _blog_sum.html ├── _bookmarklet.html ├── _item_sum.html ├── about.html ├── auth_form.html ├── base.html ├── collection.html ├── item.html ├── more_item.html ├── profile.html └── sitemap └── sitemap.xml /.github/workflows/ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: CI Ubuntu 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build_and_test: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | version: 18 | - 1.42.0 # MSRV 19 | - stable 20 | - nightly 21 | 22 | name: ${{ matrix.version }} - x86_64-unknown-linux-gnu 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | 28 | - name: Build 29 | run: cargo build --verbose 30 | 31 | # - name: Run tests 32 | # run: cargo test --verbose 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | src/.DS_Store 5 | 6 | .env 7 | migrations/.gitkeep 8 | srv.log 9 | 10 | webapp/node_modules 11 | webapp/dist 12 | webapp/e2e 13 | webapp/src/assets/.gitkeep 14 | /spa 15 | /www 16 | 17 | **/.DS_Store 18 | 19 | links.json 20 | todo.md 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "srv" 3 | version = "0.1.0" 4 | authors = ["danloh"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | futures = "0.3" 11 | actix = "0.10.0" 12 | actix-rt = "1.1" 13 | actix-web = "3.3" 14 | actix-cors = "0.4" 15 | diesel = { version = "1.4.5", features = ["postgres","chrono","serde_json","r2d2"] } 16 | r2d2 = "0.8" 17 | serde_derive = "1.0" 18 | serde_json = "1.0" 19 | serde = "1.0" 20 | 21 | jsonwebtoken = "7.2.0" 22 | lettre = "0.9" 23 | lettre_email = "0.9" 24 | # oauth2 = "=1.3.0" 25 | rand = "0.7" 26 | 27 | derive_more = "0.99" 28 | regex = "1.4.2" 29 | lazy_static = "1.4.0" 30 | deunicode = "1.1.1" 31 | bcrypt = "0.9.0" 32 | chrono = { version = "0.4.19", features = ["serde"] } 33 | chrono-tz = "0.5" 34 | 35 | log = "0.4" 36 | fern = "0.6.0" 37 | dotenv = "0.15.0" 38 | base64 = "0.13.0" 39 | num_cpus = "1.13.0" 40 | 41 | actix-files = "0.4" 42 | unic-segment = "0.9.0" 43 | pulldown-cmark = { version = "0.8.0", default-features = false } 44 | ammonia = "3" 45 | askama = "0.10" 46 | 47 | reqwest = { version = "0.10", features = ["blocking"] } 48 | scraper = "0.12.0" 49 | 50 | swirl = { git = "https://github.com/sgrif/swirl.git", rev = "de5d8bb" } 51 | parking_lot = "0.11" 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # toplog 2 | An Aggregator for Programmers Powered by Rust but not only for Rust 3 | 4 | ## Feature 5 | 6 | - Find Featured blogs on specific topic and aggregate the good posts via a simple bot; 7 | - Submit and share good articles you read to toplog by just one simple click via a bookmarklet; 8 | - vote or save the article to read again later 9 | - discuss (WIP) 10 | 11 | ## Built With 12 | 13 | - [Rust](https://www.rust-lang.org) 14 | - [Actix](https://actix.rs/) 15 | - [Diesel](http://diesel.rs/) 16 | - [Askama](https://github.com/djc/askama) 17 | - [VanillaJS](https://developer.mozilla.org/en-US/docs/Web/JavaScript) 18 | 19 | Thanks! 20 | 21 | ## Welcome To Join 22 | 23 | - you can compile and deploy freely; 24 | - Appreciate any bug report / pr / suggestion; 25 | - find good blog on any topics and define the rules to feed bot, just in [cfg.rs](https://github.com/danloh/toplog/blob/master/src/bot/cfg.rs), anyone can do it even though you are not a fan of Rust Language. 26 | 27 | -------------------------------------------------------------------------------- /diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/schema.rs" 6 | -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /migrations/2020-10-19-161234_ini/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` -------------------------------------------------------------------------------- /migrations/2020-10-19-161234_ini/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE SEQUENCE IF NOT EXISTS serial_seq 4 | INCREMENT 1 5 | MAXVALUE 2147483647 6 | START 3645 7 | CACHE 1; 8 | 9 | CREATE TABLE users ( 10 | id INTEGER PRIMARY KEY DEFAULT nextval('serial_seq'), 11 | uname VARCHAR UNIQUE NOT NULL, 12 | psw_hash VARCHAR NOT NULL, 13 | join_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | last_seen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | avatar VARCHAR NOT NULL DEFAULT '', 16 | email VARCHAR NOT NULL DEFAULT '', 17 | link VARCHAR NOT NULL DEFAULT '', 18 | intro TEXT NOT NULL DEFAULT '', 19 | location VARCHAR NOT NULL DEFAULT '', 20 | nickname VARCHAR NOT NULL DEFAULT '', 21 | permission SMALLINT NOT NULL DEFAULT 3, 22 | auth_from VARCHAR NOT NULL DEFAULT '', 23 | email_confirmed BOOLEAN NOT NULL DEFAULT FALSE, 24 | karma INTEGER NOT NULL DEFAULT 100, 25 | is_pro BOOLEAN NOT NULL DEFAULT FALSE, 26 | can_push BOOLEAN NOT NULL DEFAULT FALSE, 27 | push_email VARCHAR NOT NULL DEFAULT '', 28 | UNIQUE (uname, email) 29 | ); 30 | 31 | -- AUTHOR: OUTLET 32 | CREATE TABLE blogs ( 33 | id INTEGER PRIMARY KEY DEFAULT nextval('serial_seq'), 34 | aname VARCHAR UNIQUE NOT NULL, 35 | avatar VARCHAR NOT NULL DEFAULT '', 36 | intro VARCHAR NOT NULL DEFAULT '', 37 | topic VARCHAR NOT NULL DEFAULT '', 38 | blog_link VARCHAR NOT NULL DEFAULT '', 39 | blog_host VARCHAR NOT NULL DEFAULT '', 40 | gh_link VARCHAR NOT NULL DEFAULT '', 41 | other_link VARCHAR NOT NULL DEFAULT '', 42 | is_top BOOLEAN NOT NULL DEFAULT FALSE, 43 | karma INTEGER NOT NULL DEFAULT 1, 44 | UNIQUE (aname, blog_link) 45 | ); 46 | 47 | -- article|media|event|book| etc.. 48 | CREATE TABLE items ( 49 | id INTEGER PRIMARY KEY DEFAULT nextval('serial_seq'), 50 | title VARCHAR NOT NULL, 51 | content TEXT NOT NULL DEFAULT '', 52 | logo VARCHAR NOT NULL DEFAULT '', 53 | author VARCHAR NOT NULL, 54 | ty VARCHAR NOT NULL, -- article|media|event|book| etc.. 55 | topic VARCHAR NOT NULL DEFAULT '', 56 | link VARCHAR UNIQUE NOT NULL, 57 | link_host VARCHAR NOT NULL DEFAULT '', 58 | pub_at DATE NOT NULL DEFAULT CURRENT_DATE, 59 | post_by VARCHAR NOT NULL DEFAULT '', 60 | post_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 61 | is_top BOOLEAN NOT NULL DEFAULT FALSE, 62 | vote INTEGER NOT NULL DEFAULT 1 63 | ); 64 | 65 | CREATE INDEX items_author_idx ON items (author); 66 | 67 | -- tag 68 | CREATE TABLE labels ( 69 | id INTEGER PRIMARY KEY DEFAULT nextval('serial_seq'), 70 | label VARCHAR UNIQUE NOT NULL, 71 | slug VARCHAR UNIQUE NOT NULL, 72 | intro TEXT NOT NULL DEFAULT '', 73 | logo VARCHAR NOT NULL DEFAULT '', 74 | vote INTEGER NOT NULL DEFAULT 1 75 | ); 76 | 77 | CREATE TABLE itemlabels ( 78 | item_id INTEGER NOT NULL REFERENCES items (id) ON UPDATE CASCADE ON DELETE CASCADE, 79 | label VARCHAR NOT NULL REFERENCES labels (label) ON UPDATE CASCADE ON DELETE CASCADE, 80 | label_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 81 | PRIMARY KEY (item_id, label) 82 | ); 83 | 84 | -- origin creation 85 | CREATE TABLE comments ( 86 | id INTEGER PRIMARY KEY DEFAULT nextval('serial_seq'), 87 | title VARCHAR DEFAULT '', 88 | content TEXT NOT NULL DEFAULT '', 89 | author VARCHAR NOT NULL, 90 | post_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 91 | vote INTEGER NOT NULL DEFAULT 1, 92 | is_closed BOOLEAN NOT NULL DEFAULT FALSE, 93 | as_ty SMALLINT NOT NULL DEFAULT 1 -- 1-article, 0-comments 94 | ); 95 | 96 | CREATE TABLE itemcomments ( 97 | item_id INTEGER NOT NULL REFERENCES items (id) ON UPDATE CASCADE ON DELETE CASCADE, 98 | comment_id INTEGER NOT NULL REFERENCES comments (id) ON UPDATE CASCADE ON DELETE CASCADE, 99 | PRIMARY KEY (item_id, comment_id) 100 | ); 101 | 102 | CREATE TABLE voteitems ( 103 | uname VARCHAR NOT NULL REFERENCES users (uname) ON UPDATE CASCADE ON DELETE CASCADE, 104 | item_id INTEGER NOT NULL REFERENCES items (id) ON UPDATE CASCADE ON DELETE CASCADE, 105 | vote_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 106 | vote_as SMALLINT NOT NULL DEFAULT 1 CHECK (vote_as = 1 OR vote_as = -1), 107 | PRIMARY KEY (uname, item_id) 108 | ); 109 | 110 | CREATE TABLE votecomments ( 111 | uname VARCHAR NOT NULL REFERENCES users (uname) ON UPDATE CASCADE ON DELETE CASCADE, 112 | comment_id INTEGER NOT NULL REFERENCES comments (id) ON UPDATE CASCADE ON DELETE CASCADE, 113 | vote_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 114 | vote_as SMALLINT NOT NULL DEFAULT 1 CHECK (vote_as = 1 OR vote_as = -1), 115 | PRIMARY KEY (uname, comment_id) 116 | ); 117 | 118 | CREATE TABLE background_jobs ( 119 | id BIGSERIAL PRIMARY KEY, 120 | job_type TEXT NOT NULL, 121 | data JSONB NOT NULL, 122 | retries INTEGER NOT NULL DEFAULT 0, 123 | last_retry TIMESTAMP NOT NULL DEFAULT '1970-01-01', 124 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 125 | ); 126 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 85 2 | # wrap_comments = true -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | pub mod auth; 3 | pub mod blog; 4 | pub mod item; 5 | 6 | #[derive(Deserialize, Clone)] 7 | pub struct ReqQuery { 8 | per: String, 9 | kw: String, 10 | page: i32, 11 | perpage: i32, 12 | } 13 | 14 | #[derive(Deserialize, Clone)] 15 | pub struct ActionQuery { 16 | action: String, 17 | } 18 | 19 | use crate::api::auth::CheckUser; 20 | use crate::api::item::Item; 21 | 22 | // general response msg struct 23 | #[derive(Deserialize, Serialize, Debug)] 24 | pub struct Msg { 25 | pub status: i32, 26 | pub message: String, 27 | } 28 | 29 | // msg for login 30 | #[derive(Deserialize, Serialize, Debug)] 31 | pub struct AuthMsg { 32 | pub status: i32, 33 | pub message: String, 34 | pub token: String, 35 | pub exp: i32, 36 | pub user: CheckUser, 37 | pub omg: bool, // if it is the admin 38 | } 39 | 40 | #[derive(Deserialize, Serialize, Debug)] 41 | pub struct ItemsMsg { 42 | pub items: Vec, 43 | pub count: i64, 44 | } 45 | 46 | // msg for get user info 47 | #[derive(Deserialize, Serialize, Debug)] 48 | pub struct UserMsg { 49 | pub status: i32, 50 | pub message: String, 51 | pub user: CheckUser, 52 | } 53 | 54 | // ================================================================================= 55 | // ================================================================================= 56 | // regex 57 | 58 | use regex::Regex; 59 | 60 | // re test 61 | // for re test uname 62 | pub fn re_test_name(text: &str) -> bool { 63 | lazy_static! { 64 | static ref RE: Regex = 65 | Regex::new(r"^[\w-]{3,18}$").unwrap(); // let fail in test 66 | } 67 | RE.is_match(text) 68 | } 69 | 70 | // for re test psw 71 | pub fn re_test_psw(text: &str) -> bool { 72 | lazy_static! { 73 | static ref RE: Regex = 74 | Regex::new(r"^[\w#@~%^$&*-]{8,18}$").unwrap(); // let fail in test 75 | } 76 | RE.is_match(text) 77 | } 78 | 79 | // for re test url 80 | pub fn re_test_url(text: &str) -> bool { 81 | lazy_static! { 82 | static ref RE: Regex = 83 | Regex::new(r"^(https?)://([^/:]+)(:[0-9]+)?(/.*)?$").unwrap(); // let fail in test 84 | } 85 | RE.is_match(text) 86 | } 87 | 88 | pub fn re_test_img_url(text: &str) -> bool { 89 | lazy_static! { 90 | static ref RE: Regex = 91 | Regex::new(r"^https?://.+\.(jpg|gif|png|svg)$").unwrap(); // let fail in test 92 | } 93 | RE.is_match(text) 94 | } 95 | 96 | // for re test email 97 | pub fn re_test_email(text: &str) -> bool { 98 | lazy_static! { 99 | static ref RE: Regex = 100 | Regex::new(r"^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$").unwrap(); // let fail in test 101 | } 102 | RE.is_match(text) 103 | } 104 | 105 | pub fn replace_sep(text: &str, rep: &str) -> String { 106 | lazy_static! { 107 | static ref RE: Regex = 108 | Regex::new(r"[^a-zA-Z0-9]").unwrap(); // let fail in test 109 | } 110 | RE.replace_all(text, rep).into_owned() 111 | } 112 | 113 | pub fn replace_all_whitespace(text: &str, rep: &str) -> String { 114 | lazy_static! { 115 | static ref RE: Regex = 116 | Regex::new(r" ").unwrap(); // let fail in test 117 | } 118 | RE.replace_all(text, rep).into_owned() 119 | } 120 | 121 | pub fn replace_sep_tag(text: &str, rep: &str) -> String { 122 | lazy_static! { 123 | static ref RE: Regex = Regex::new( 124 | r"[`~!@#$%^&*()+=|{}\]\[':;,.\\?/<>《》;:。,“‘’”【】「」——()……¥!~·]" // same to frontend 125 | ) 126 | .unwrap(); // let fail in test, "?? 127 | } 128 | RE.replace_all(text, rep).into_owned() 129 | } 130 | 131 | pub fn trim_url_qry(text: &str, rep: &str) -> String { 132 | lazy_static! { 133 | static ref RE: Regex = Regex::new(r"/ref=.*").unwrap(); // let fail in test 134 | } 135 | RE.replace_all(text, rep).into_owned() 136 | } 137 | 138 | pub fn test_len_limit(text: &str, min: usize, max: usize) -> bool { 139 | let l = text.trim().len(); 140 | l >= min && l <= max 141 | } 142 | 143 | // some const to eliminate magic number 144 | pub const PER_PAGE: i32 = 20; // for paging 145 | pub const TITLE_LEN: usize = 256; 146 | //pub const URL_LEN: usize = 256; 147 | //pub const UIID_LEN: usize = 32; 148 | pub const TAG_LEN: usize = 42; 149 | //pub const ST_LEN: usize = 16; // for some short input: category 150 | pub const MID_LEN: usize = 32; // for some mid input: lcoation 151 | //pub const LG_LEN: usize = 64; // for sone longer input: 152 | -------------------------------------------------------------------------------- /src/bin/job.rs: -------------------------------------------------------------------------------- 1 | // put background jobs to queue 2 | 3 | use srv::errors::{SrvError, SrvResult}; 4 | use srv::{db, bot::tasks}; 5 | use swirl::Job; 6 | 7 | fn main() -> SrvResult<()> { 8 | let conn = db::connect_now()?; 9 | 10 | tasks::spider_items() 11 | .enqueue(&conn) 12 | .map_err(|e| SrvError::from_std_error(e))?; 13 | 14 | tasks::cal_blogs_karma() 15 | .enqueue(&conn) 16 | .map_err(|e| SrvError::from_std_error(e))?; 17 | 18 | tasks::gen_static_site() 19 | .enqueue(&conn) 20 | .map_err(|e| SrvError::from_std_error(e))?; 21 | 22 | println!("enqueue tasks"); 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /src/bin/serve.rs: -------------------------------------------------------------------------------- 1 | // web server bin 2 | 3 | use srv::init_server; 4 | 5 | #[actix_rt::main] 6 | async fn main() -> std::io::Result<()> { 7 | 8 | /* 9 | // to test spider works 10 | use srv::bot::spider::{WebPage}; 11 | let r = WebPage::new("https://levpaul.com/"); 12 | let links = dbg!(r.unwrap_or_default().clean_links()); 13 | println!("{:#?}", links); 14 | let item = WebPage::new(&links[0]).unwrap_or_default().into_item(); 15 | println!("{:#?}", item); 16 | // end 17 | */ 18 | 19 | init_server().await 20 | } 21 | -------------------------------------------------------------------------------- /src/bin/worker.rs: -------------------------------------------------------------------------------- 1 | // enqueued background jobs runner bin 2 | 3 | use srv::bot::jobs::Environment; 4 | use srv::db; 5 | 6 | use diesel::r2d2; 7 | use std::thread::sleep; 8 | use std::time::Duration; 9 | 10 | fn main() { 11 | println!(">>> Booting Background jobs runner"); 12 | 13 | let db_url = dotenv::var("DATABASE_URL").expect("DATABASE_URL must be set"); 14 | let db_config = r2d2::Pool::builder().max_size(4); 15 | let db_pool = db::diesel_pool(db_url, db_config); 16 | 17 | let job_start_timeout = dotenv::var("BACKGROUND_JOB_TIMEOUT") 18 | .unwrap_or_else(|_| "30".into()) 19 | .parse() 20 | .expect("Invalid value for `BACKGROUND_JOB_TIMEOUT`"); 21 | 22 | let environment = Environment::new(db_pool.clone()); 23 | 24 | let build_runner = || { 25 | swirl::Runner::builder(db_pool.clone(), environment.clone()) 26 | .thread_count(2) 27 | .job_start_timeout(Duration::from_secs(job_start_timeout)) 28 | .build() 29 | }; 30 | let mut runner = build_runner(); 31 | 32 | println!(">>> Runner booted, running jobs"); 33 | 34 | let mut failure_count = 0; 35 | 36 | loop { 37 | if let Err(e) = runner.run_all_pending_jobs() { 38 | log::error!("back worker: {}", e); 39 | failure_count += 1; 40 | if failure_count < 5 { 41 | eprintln!( 42 | ">>!! Error running jobs (n = {}) -- retrying: {:?}", 43 | failure_count, e, 44 | ); 45 | runner = build_runner(); 46 | } else { 47 | panic!("!! Failed to run jobs 5 times. Restarting the process"); 48 | } 49 | } 50 | sleep(Duration::from_secs(1)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/bot/jobs.rs: -------------------------------------------------------------------------------- 1 | // background jobs setup 2 | 3 | use std::error::Error; 4 | use std::panic::AssertUnwindSafe; 5 | use std::sync::Arc; 6 | use swirl::PerformError; 7 | 8 | use crate::db::{DieselPool, DieselPooledConn}; 9 | use crate::errors::{SrvErrToStdErr, SrvError, SrvResult}; 10 | 11 | impl<'a> swirl::db::BorrowedConnection<'a> for DieselPool { 12 | type Connection = DieselPooledConn<'a>; 13 | } 14 | 15 | impl swirl::db::DieselPool for DieselPool { 16 | type Error = SrvErrToStdErr; 17 | 18 | fn get(&self) -> Result, Self::Error> { 19 | self.get().map_err(SrvErrToStdErr) 20 | } 21 | } 22 | 23 | #[allow(missing_debug_implementations)] 24 | pub struct Environment { 25 | // FIXME: https://github.com/sfackler/r2d2/pull/70 26 | pub conn_pool: AssertUnwindSafe, 27 | } 28 | 29 | // FIXME: AssertUnwindSafe should be `Clone`, this can be replaced with 30 | // `#[derive(Clone)]` if that is fixed in the standard lib 31 | impl Clone for Environment { 32 | fn clone(&self) -> Self { 33 | Self { 34 | conn_pool: AssertUnwindSafe(self.conn_pool.0.clone()), 35 | } 36 | } 37 | } 38 | 39 | impl Environment { 40 | pub fn new(conn_pool: DieselPool) -> Self { 41 | Self { 42 | conn_pool: AssertUnwindSafe(conn_pool), 43 | } 44 | } 45 | 46 | pub fn connection(&self) -> Result, PerformError> { 47 | self.conn_pool.get().map_err(|e| SrvErrToStdErr(e).into()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/bot/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | pub mod jobs; 3 | pub mod spider; 4 | pub mod cfg; 5 | pub mod tasks; 6 | -------------------------------------------------------------------------------- /src/bot/spider.rs: -------------------------------------------------------------------------------- 1 | // a simple page crawle 2 | 3 | use regex::Regex; 4 | use chrono::{NaiveDate, Utc}; 5 | use scraper::{Html, Selector}; 6 | 7 | use crate::errors::{ServiceError, ServiceResult}; 8 | use crate::api::item::NewItem; 9 | use crate::api::{re_test_img_url, replace_sep, trim_url_qry}; 10 | use crate::util::helper::gen_slug; 11 | use crate::bot::cfg::{get_links, MAP_HOST}; 12 | 13 | #[derive(Deserialize, Serialize, Debug, Clone)] 14 | pub struct PageInfo { 15 | pub title: String, 16 | pub url: String, 17 | pub img: String, 18 | pub content: String, 19 | } 20 | 21 | #[derive(Debug, Clone, Default)] 22 | pub struct WebPage { 23 | pub url: String, 24 | pub html: String, 25 | pub domain: String, 26 | } 27 | 28 | impl WebPage { 29 | pub fn new(url: &str) -> ServiceResult { 30 | // let res = reqwest::get(url)?.text()?; 31 | 32 | let default_html = String::from(""); 33 | let res = match reqwest::blocking::get(url) { 34 | Ok(resp) => { 35 | let mut resp = resp; 36 | match resp.text() { 37 | Ok(s) => s, 38 | _ => default_html 39 | } 40 | }, 41 | _ => default_html 42 | }; 43 | 44 | lazy_static! { 45 | static ref Scheme_re: Regex = Regex::new(r"https?://").unwrap(); 46 | static ref Path_re: Regex = Regex::new(r"/.*").unwrap(); 47 | } 48 | 49 | let uri = Scheme_re.replace_all(url, ""); 50 | let host = Path_re.replace_all(&uri, ""); 51 | let domain = host.replace("www.", ""); 52 | 53 | let page = Self { 54 | url: url.to_string(), 55 | html: res, 56 | domain, 57 | }; 58 | Ok(page) 59 | } 60 | 61 | // URL getter 62 | pub fn get_url(&self) -> String { 63 | self.url.clone() 64 | } 65 | 66 | // Domain getter 67 | pub fn get_domain(&self) -> String { 68 | self.domain.clone() 69 | } 70 | 71 | // HTML parser 72 | pub fn get_html(&self) -> Html { 73 | Html::parse_document(&self.html) 74 | } 75 | 76 | // extract links in html page 77 | pub fn extract_links(&self, selector_str: &str) -> Vec { 78 | let link_selector = match Selector::parse(selector_str) { 79 | Ok(selector) => selector, 80 | _ => return Vec::new() 81 | }; 82 | 83 | let html = self.get_html(); 84 | let link_refs: Vec<_> = html.select(&link_selector).collect(); 85 | let mut links: Vec = Vec::new(); 86 | for link in link_refs { 87 | match link.value().attr("href") { 88 | Some(href) => { 89 | links.push(href.to_string()) 90 | } 91 | None => {} 92 | } 93 | } 94 | 95 | links 96 | } 97 | 98 | pub fn clean_links(&self) -> Vec { 99 | get_links(self) 100 | } 101 | 102 | pub fn into_item(&self) -> NewItem { 103 | let url = self.get_url(); 104 | let html = self.get_html(); 105 | let domain = self.get_domain(); 106 | let dmn = domain.trim(); 107 | match dmn { 108 | _ => { 109 | let page = parse_common_page(html, &url); 110 | let title = page.title.trim(); 111 | NewItem { 112 | title: replace_space(title, " "), 113 | content: page.content.trim().to_owned(), 114 | logo: page.img.trim().to_owned(), 115 | author: get_author_topic(dmn).0, 116 | ty: "Article".to_owned(), 117 | topic: get_author_topic(dmn).1, 118 | link: page.url.trim().to_owned(), 119 | post_by: "bot".to_owned(), 120 | pub_at: Utc::today().naive_utc() 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | pub fn page_ele_paser( 128 | html: &Html, 129 | sel_str: &str, 130 | attr_str: &str, 131 | alt_txt: &str, 132 | ) -> Vec { 133 | let a_selector = match Selector::parse(sel_str) { 134 | Ok(selector) => selector, 135 | _ => return Vec::new() 136 | }; 137 | let a_vec: Vec<_> = html.select(&a_selector).collect(); 138 | 139 | if a_vec.len() == 0 { 140 | return Vec::new(); 141 | } 142 | 143 | let mut a_txt_vec = Vec::new(); 144 | for a in a_vec { 145 | let a_txt = if attr_str.trim().len() == 0 { 146 | a.inner_html() 147 | } else { 148 | match a.value().attr(attr_str) { 149 | Some(s) => s.to_owned(), 150 | None => String::from(alt_txt), 151 | } 152 | }; 153 | a_txt_vec.push(a_txt); 154 | } 155 | 156 | // test 157 | // println!(">>{:?} -> {:?}", sel_str, a_txt_vec); 158 | 159 | a_txt_vec 160 | } 161 | 162 | pub fn parse_common_page(html: Html, url: &str) -> PageInfo { 163 | 164 | // get title 165 | let title_text: String = 166 | page_ele_paser( 167 | &html, "head > title", "", url 168 | ) 169 | .first() 170 | .unwrap_or(&String::from(url)) 171 | .to_string(); 172 | 173 | // get image url 174 | // 175 | // og:image og:image:url 176 | let og_img: String = page_ele_paser( 177 | &html, r#"meta[property="og:image"]"#, "content", "" 178 | ) 179 | .first() 180 | .unwrap_or(&String::from("")) 181 | .to_string(); 182 | 183 | let img_src: String = og_img; 184 | // if og_img.len() == 0 { 185 | // // random body img 186 | // page_ele_paser( 187 | // &html, "body img", "src", "" 188 | // ) 189 | // .first() 190 | // .unwrap_or(&String::from("")) 191 | // .to_string() 192 | // } else { 193 | // og_img 194 | // }; 195 | 196 | // get content descript -- meta description or og:description 197 | // 198 | // meta description 199 | let meta_descript: String = page_ele_paser( 200 | &html, r#"meta[name="description"]"#, "content", "" 201 | ) 202 | .first() 203 | .unwrap_or(&String::from("")) 204 | .to_string(); 205 | 206 | let content: String = if meta_descript.len() == 0 { 207 | // og:description 208 | page_ele_paser( 209 | &html, r#"meta[property="og:description"]"#, "content", "" 210 | ) 211 | .first() 212 | .unwrap_or(&String::from("")) 213 | .to_string() 214 | } else { 215 | meta_descript 216 | }; 217 | 218 | // get canonical link 219 | let c_link: String = page_ele_paser( 220 | &html, r#"link[rel="canonical"]"#, "href", url 221 | ) 222 | .first() 223 | .unwrap_or(&String::from(url)) 224 | .to_string(); 225 | 226 | PageInfo { 227 | title: title_text, 228 | url: c_link, 229 | img: img_src, 230 | content, 231 | } 232 | } 233 | 234 | // some helpers 235 | 236 | fn get_author_topic(host: &str) -> (String, String) { 237 | let map = &MAP_HOST; 238 | let default = &(host, "Rust"); 239 | let tup = map.get(host).unwrap_or(default); 240 | 241 | (tup.0.to_owned(), tup.1.to_owned()) 242 | } 243 | 244 | // replace whitespance, \n \t \r \f... 245 | pub fn replace_space(text: &str, rep: &str) -> String { 246 | lazy_static! { 247 | static ref RE: Regex = 248 | Regex::new(r"\s").unwrap(); // let fail in test 249 | } 250 | RE.replace_all(text, rep).into_owned() 251 | } 252 | -------------------------------------------------------------------------------- /src/bot/tasks.rs: -------------------------------------------------------------------------------- 1 | use crate::bot::jobs::Environment; 2 | use crate::errors::{SrvErrToStdErr, SrvError, SrvResult}; 3 | use crate::api::{ 4 | item::{NewItem, Item}, 5 | blog::{Blog}, 6 | }; 7 | use diesel::dsl::{any, sum}; 8 | use diesel::prelude::*; 9 | use std::error::Error; 10 | use swirl::PerformError; 11 | 12 | // =============================================================== 13 | // spider 14 | // 15 | // spider item and save to db 16 | #[swirl::background_job] 17 | pub fn spider_items(env: &Environment) -> Result<(), PerformError> { 18 | let conn = env.connection()?; 19 | spider_and_save_item(&conn)?; 20 | 21 | Ok(()) 22 | } 23 | 24 | pub fn spider_and_save_item(conn: &PgConnection) -> QueryResult<()> { 25 | use crate::schema::items::dsl::*; 26 | use crate::bot::spider::{WebPage}; 27 | 28 | // new WebPages and get all links 29 | let mut links: Vec = Vec::new(); 30 | use crate::bot::cfg::LINK_VEC; 31 | let url_list = &LINK_VEC; 32 | for url in url_list.iter() { 33 | // println!("{}", url); 34 | let page = WebPage::new(url).unwrap_or_default(); 35 | links.append(&mut page.clean_links()); 36 | } 37 | // println!("{:?}", links); 38 | 39 | // diff the links w/ db 40 | // 41 | // extracted new links 42 | use std::collections::HashSet; 43 | let mut links_set = HashSet::new(); 44 | for l in links { 45 | // regex check url 46 | use crate::api::re_test_url; 47 | if re_test_url(&l) { 48 | links_set.insert(l); 49 | } 50 | } 51 | 52 | // deserde spidered links from Json as reduce query db 53 | // 54 | use crate::util::helper::{deserde_links, serde_links, serde_add_links}; 55 | let sped_links = deserde_links(); 56 | let sp_links = if sped_links.len() > 0 { 57 | sped_links 58 | } else { 59 | let db_links: Vec = items 60 | .filter(link.ne("")) 61 | //.filter(post_at.lt(limit_day)) // did nothing 62 | .select(link) 63 | .load::(conn)?; 64 | serde_links(db_links.clone()); 65 | db_links 66 | }; 67 | let mut spd_links_set = HashSet::new(); 68 | for l in sp_links { 69 | spd_links_set.insert(l); 70 | } 71 | 72 | // diff the real new links to feed spider 73 | let diff_links = links_set.difference(&spd_links_set); 74 | //println!("{:#?}", diff_links); 75 | let mut new_links: Vec = Vec::new(); 76 | // spider the diff_links and build item 77 | let mut new_items: Vec = Vec::new(); 78 | for l in diff_links { 79 | let sp_item = WebPage::new(l) 80 | .unwrap_or_default() 81 | .into_item(); 82 | new_items.push(sp_item); 83 | new_links.push(l.to_string()); 84 | } 85 | 86 | // save new items to db 87 | diesel::insert_into(items) 88 | .values(&new_items) 89 | .on_conflict_do_nothing() 90 | .execute(conn)?; 91 | 92 | // save new links to json 93 | serde_add_links(new_links); 94 | 95 | Ok(()) 96 | } 97 | 98 | 99 | // Cal 100 | // 101 | // cal blog karma 102 | #[swirl::background_job] 103 | pub fn cal_blogs_karma(env: &Environment) -> Result<(), PerformError> { 104 | let conn = env.connection()?; 105 | update_blogs_karma(&conn)?; 106 | 107 | Ok(()) 108 | } 109 | 110 | pub fn update_blogs_karma(conn: &PgConnection) -> QueryResult<()> { 111 | use crate::schema::blogs::dsl::*; 112 | use crate::schema::items::dsl::{items, author, vote}; 113 | let blog_list = blogs.load::(conn)?; 114 | for b in blog_list { 115 | let bname = &b.aname; 116 | let votes: Vec = items 117 | .filter(author.eq(bname)) 118 | .select(vote) 119 | .load::(conn)?; 120 | let k: i32 = votes.iter().sum(); 121 | let threshold: i32 = dotenv::var("THRESHOLD") 122 | .unwrap_or("42".to_owned()) 123 | .parse().unwrap_or(42); 124 | let if_top = k > threshold || b.is_top; 125 | diesel::update(&b) 126 | .set(( 127 | karma.eq(k), 128 | is_top.eq(if_top) 129 | )) 130 | .execute(conn)?; 131 | } 132 | 133 | Ok(()) 134 | } 135 | 136 | 137 | // statify the site 138 | // 139 | #[swirl::background_job] 140 | pub fn gen_static_site(_env: &Environment) -> Result<(), PerformError> { 141 | 142 | use crate::view::tmpl::del_dir; 143 | del_dir("www/collection").unwrap_or(()); 144 | del_dir("www/item").unwrap_or(()); 145 | 146 | Ok(()) 147 | } 148 | -------------------------------------------------------------------------------- /src/db.rs: -------------------------------------------------------------------------------- 1 | // database pool 2 | 3 | use diesel::prelude::*; 4 | use diesel::r2d2::{self, ConnectionManager, CustomizeConnection}; 5 | use parking_lot::{ReentrantMutex, ReentrantMutexGuard}; 6 | use std::ops::Deref; 7 | use std::sync::Arc; 8 | 9 | use crate::errors::SrvResult; 10 | 11 | #[allow(missing_debug_implementations)] 12 | #[derive(Clone)] 13 | pub enum DieselPool { 14 | Pool(r2d2::Pool>), 15 | Test(Arc>), 16 | } 17 | 18 | impl DieselPool { 19 | pub fn get(&self) -> SrvResult> { 20 | match self { 21 | DieselPool::Pool(pool) => Ok(DieselPooledConn::Pool(pool.get()?)), 22 | DieselPool::Test(conn) => Ok(DieselPooledConn::Test(conn.lock())), 23 | } 24 | } 25 | 26 | pub fn state(&self) -> r2d2::State { 27 | match self { 28 | DieselPool::Pool(pool) => pool.state(), 29 | DieselPool::Test(_) => panic!("Cannot get the state of a test pool"), 30 | } 31 | } 32 | 33 | fn test_conn(conn: PgConnection) -> Self { 34 | DieselPool::Test(Arc::new(ReentrantMutex::new(conn))) 35 | } 36 | } 37 | 38 | #[allow(missing_debug_implementations)] 39 | pub enum DieselPooledConn<'a> { 40 | Pool(r2d2::PooledConnection>), 41 | Test(ReentrantMutexGuard<'a, PgConnection>), 42 | } 43 | 44 | unsafe impl<'a> Send for DieselPooledConn<'a> {} 45 | 46 | impl Deref for DieselPooledConn<'_> { 47 | type Target = PgConnection; 48 | 49 | fn deref(&self) -> &Self::Target { 50 | match self { 51 | DieselPooledConn::Pool(conn) => conn.deref(), 52 | DieselPooledConn::Test(conn) => conn.deref(), 53 | } 54 | } 55 | } 56 | 57 | pub fn connect_now() -> ConnectionResult { 58 | let url = dotenv::var("DATABASE_URL").expect("Invalid database URL"); 59 | PgConnection::establish(&url.to_string()) 60 | } 61 | 62 | pub fn diesel_pool( 63 | url: String, 64 | config: r2d2::Builder>, 65 | ) -> DieselPool { 66 | let manager = ConnectionManager::new(url); 67 | DieselPool::Pool(config.build(manager).unwrap()) 68 | } 69 | 70 | #[derive(Debug, Clone, Copy)] 71 | pub struct ConnectionConfig { 72 | pub statement_timeout: u64, 73 | pub read_only: bool, 74 | } 75 | 76 | impl CustomizeConnection for ConnectionConfig { 77 | fn on_acquire(&self, conn: &mut PgConnection) -> Result<(), r2d2::Error> { 78 | use diesel::sql_query; 79 | 80 | sql_query(format!( 81 | "SET statement_timeout = {}", 82 | self.statement_timeout * 1000 83 | )) 84 | .execute(conn) 85 | .map_err(r2d2::Error::QueryError)?; 86 | if self.read_only { 87 | sql_query("SET default_transaction_read_only = 't'") 88 | .execute(conn) 89 | .map_err(r2d2::Error::QueryError)?; 90 | } 91 | Ok(()) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings)] 2 | 3 | #[macro_use] 4 | extern crate serde_derive; 5 | #[macro_use] 6 | extern crate diesel; 7 | #[macro_use] 8 | extern crate lazy_static; 9 | 10 | use actix::prelude::*; 11 | use actix::{Actor, SyncContext}; 12 | use actix_cors::Cors; 13 | use actix_files as fs; 14 | use actix_web::{ 15 | middleware::{Compress, Logger}, 16 | web::{delete, get, post, put, patch, resource, route, scope}, 17 | App, HttpResponse, HttpServer, 18 | }; 19 | 20 | use diesel::pg::PgConnection; 21 | use diesel::r2d2::{ConnectionManager, Pool}; 22 | 23 | // #[macro_use] 24 | // pub mod macros; 25 | 26 | pub mod api; 27 | pub mod errors; 28 | pub mod schema; 29 | pub mod util; 30 | pub mod view; 31 | pub mod bot; 32 | pub mod db; 33 | 34 | // some type alias 35 | pub type PoolConn = Pool>; 36 | pub type PooledConn = r2d2::PooledConnection>; 37 | 38 | // This is db executor actor 39 | pub struct Dba(pub Pool>); 40 | 41 | impl Actor for Dba { 42 | type Context = SyncContext; 43 | } 44 | 45 | pub type DbAddr = Addr; 46 | 47 | pub fn init_dba() -> DbAddr { 48 | let db_url = dotenv::var("DATABASE_URL").expect("DATABASE_URL must be set"); 49 | let manager = ConnectionManager::::new(db_url); 50 | let cpu_num = num_cpus::get(); 51 | let pool_num = std::cmp::max(10, cpu_num * 2 + 1) as u32; 52 | // p_num subject to c_num?? 53 | let conn = Pool::builder() 54 | .max_size(pool_num) 55 | .build(manager) 56 | .expect("Failed to create pool."); 57 | 58 | SyncArbiter::start(cpu_num * 2 + 1, move || Dba(conn.clone())) 59 | } 60 | 61 | pub fn init_fern_logger() -> Result<(), fern::InitError> { 62 | fern::Dispatch::new() 63 | .format(|out, message, record| { 64 | out.finish(format_args!( 65 | "{}[{}][{},{}] {}", 66 | chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), 67 | record.level(), 68 | record.target(), 69 | record.line().unwrap_or(0), 70 | message 71 | )) 72 | }) 73 | .level(log::LevelFilter::Info) 74 | .chain(fern::log_file("srv.log")?) 75 | .apply()?; 76 | 77 | Ok(()) 78 | } 79 | 80 | //#[actix_rt::main] 81 | pub async fn init_server() -> std::io::Result<()> { 82 | // init logger 83 | init_fern_logger().unwrap_or_default(); 84 | 85 | // new runtime manual 86 | //let sys = actix_rt::System::new("server"); 87 | 88 | // init actor 89 | let addr: DbAddr = init_dba(); 90 | 91 | let bind_host = 92 | dotenv::var("BIND_ADDRESS").unwrap_or("127.0.0.1:8085".to_string()); 93 | // config Server, App, AppState, middleware, service 94 | HttpServer::new(move || { 95 | App::new() 96 | .data(addr.clone()) 97 | .wrap(Logger::default()) 98 | .wrap(Compress::default()) 99 | .wrap(Cors::default()) 100 | // everything under '/api/' route 101 | .service(scope("/api") 102 | // to auth 103 | .service( 104 | resource("/signin") 105 | .route(post().to(api::auth::signin)) 106 | ) 107 | // to register 108 | .service( 109 | resource("/signup") 110 | .route(post().to(api::auth::signup)) 111 | ) 112 | .service( 113 | resource("/reset") // reset-1: request rest psw, send mail 114 | .route(post().to(api::auth::reset_psw_req)) 115 | ) 116 | .service( 117 | resource("/reset/{token}") // reset-2: copy token, new psw 118 | .route(post().to(api::auth::reset_psw)) 119 | ) 120 | .service( 121 | resource("/users/{uname}") 122 | .route(get().to(api::auth::get)) 123 | .route(post().to(api::auth::update)) 124 | .route(put().to(api::auth::change_psw)) 125 | ) 126 | .service( 127 | resource("/blogs") 128 | .route(post().to(api::blog::new)) 129 | .route(put().to(api::blog::update)) 130 | // get_list: ?per=topic&kw=&perpage=&page=p 131 | .route(get().to(api::blog::get_list)) 132 | ) 133 | .service( 134 | resource("/blogs/{id}") 135 | .route(get().to(api::blog::get)) 136 | .route(put().to(api::blog::toggle_top)) 137 | .route(delete().to(api::blog::del)) 138 | ) 139 | .service( 140 | resource("/items") 141 | .route(post().to(api::item::new)) 142 | .route(put().to(api::item::update)) 143 | ) 144 | .service( 145 | resource("/spider") 146 | .route(put().to(api::item::spider)) 147 | ) 148 | .service( 149 | resource("/getitems/{pper}") 150 | // get_list: ?per=topic&kw=&perpage=20&page=p 151 | .route(get().to(api::item::get_list)) 152 | ) 153 | .service( 154 | resource("/items/{id}") 155 | .route(get().to(api::item::get)) 156 | .route(patch().to(api::item::toggle_top)) 157 | // vote or veto: ?action=vote|veto 158 | .route(put().to(api::item::vote_or_veto)) 159 | .route(delete().to(api::item::del)) 160 | ) 161 | .service( 162 | resource("/generate-sitemap") 163 | .route(get().to(view::tmpl::gen_sitemap)) 164 | ) 165 | .service( 166 | resource("/generate-staticsite") 167 | .route(get().to(view::tmpl::statify_site)) 168 | ) 169 | // .service( 170 | // resource("/stfile/{p}") 171 | // .route(delete().to(view::tmpl::del_static_file)) 172 | // ) 173 | .default_service(route().to(|| HttpResponse::NotFound())) 174 | ) 175 | .service( 176 | fs::Files::new("/static", "./static/") // static files 177 | .default_handler(route().to(|| HttpResponse::NotFound())) 178 | ) 179 | .service( 180 | resource("/confirm/{token}") 181 | .route(get().to(api::auth::confirm_email)) 182 | ) 183 | .service( 184 | resource("/index") 185 | .route(get().to(view::tmpl::dyn_index)) 186 | ) 187 | .service( 188 | resource("/from") // query: ?by=&site=&ord= 189 | .route(get().to(view::tmpl::item_from)) 190 | ) 191 | .service( 192 | resource("/collection") // query: ty=&tpc=&ord= 193 | .route(get().to(view::tmpl::collection_either)) 194 | ) 195 | .service( 196 | resource("/collection/dyn") // query: ty=&tpc=&ord= 197 | .route(get().to(view::tmpl::collection_dyn)) 198 | ) 199 | .service( 200 | resource("/moreitems/{topic}/{ty}") // ?page=&perpage=42 201 | .route(get().to(view::tmpl::more_item)) 202 | ) 203 | .service( 204 | resource("/item/{id}") 205 | .route(get().to(view::tmpl::item_view_either)) 206 | ) 207 | .service( 208 | resource("/item/{id}/dyn") 209 | .route(get().to(view::tmpl::item_view_dyn)) 210 | ) 211 | .service( 212 | resource("/@{uname}") 213 | .route(get().to(view::tmpl::profile)) 214 | ) 215 | .service( 216 | resource("/site/{name}") 217 | .route(get().to(view::tmpl::site)) 218 | ) 219 | .service( 220 | resource("/auth") // query: ?to= 221 | .route(get().to(view::form::auth_form)) 222 | ) 223 | .service( 224 | resource("/newitem") 225 | .route(get().to(view::form::new_item)) 226 | ) 227 | .service( 228 | resource("/edititem") // query: ?id= 229 | .route(get().to(view::form::edit_item)) 230 | ) 231 | .service( 232 | resource("/newblog") 233 | .route(get().to(view::form::new_blog)) 234 | ) 235 | .service( 236 | resource("/editblog") // query: ?id= 237 | .route(get().to(view::form::edit_blog)) 238 | ) 239 | .service( 240 | resource("/submit") // query: extract data 241 | .route(get().to(view::form::submit_to)) 242 | ) 243 | .service( 244 | fs::Files::new("/", "./www/") // for robots.txt, sitemap 245 | .index_file("all-index.html") 246 | .default_handler(route().to(view::tmpl::dyn_index)) 247 | ) 248 | .default_service(route().to(|| HttpResponse::NotFound())) 249 | }) 250 | .bind(&bind_host) 251 | .expect("Can not bind to host") 252 | .run() 253 | .await; 254 | 255 | println!("Starting http server: {}", bind_host); 256 | 257 | // start runtime manual 258 | //sys.run() 259 | 260 | Ok(()) 261 | } 262 | -------------------------------------------------------------------------------- /src/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | background_jobs (id) { 3 | id -> Int8, 4 | job_type -> Text, 5 | data -> Jsonb, 6 | retries -> Int4, 7 | last_retry -> Timestamp, 8 | created_at -> Timestamp, 9 | } 10 | } 11 | 12 | table! { 13 | blogs (id) { 14 | id -> Int4, 15 | aname -> Varchar, 16 | avatar -> Varchar, 17 | intro -> Varchar, 18 | topic -> Varchar, 19 | blog_link -> Varchar, 20 | blog_host -> Varchar, 21 | gh_link -> Varchar, 22 | other_link -> Varchar, 23 | is_top -> Bool, 24 | karma -> Int4, 25 | } 26 | } 27 | 28 | table! { 29 | comments (id) { 30 | id -> Int4, 31 | title -> Nullable, 32 | content -> Text, 33 | author -> Varchar, 34 | post_at -> Timestamp, 35 | vote -> Int4, 36 | is_closed -> Bool, 37 | as_ty -> Int2, 38 | } 39 | } 40 | 41 | table! { 42 | itemcomments (item_id, comment_id) { 43 | item_id -> Int4, 44 | comment_id -> Int4, 45 | } 46 | } 47 | 48 | table! { 49 | itemlabels (item_id, label) { 50 | item_id -> Int4, 51 | label -> Varchar, 52 | label_at -> Timestamp, 53 | } 54 | } 55 | 56 | table! { 57 | items (id) { 58 | id -> Int4, 59 | title -> Varchar, 60 | content -> Text, 61 | logo -> Varchar, 62 | author -> Varchar, 63 | ty -> Varchar, 64 | topic -> Varchar, 65 | link -> Varchar, 66 | link_host -> Varchar, 67 | pub_at -> Date, 68 | post_by -> Varchar, 69 | post_at -> Timestamp, 70 | is_top -> Bool, 71 | vote -> Int4, 72 | } 73 | } 74 | 75 | table! { 76 | labels (id) { 77 | id -> Int4, 78 | label -> Varchar, 79 | slug -> Varchar, 80 | intro -> Text, 81 | logo -> Varchar, 82 | vote -> Int4, 83 | } 84 | } 85 | 86 | table! { 87 | users (id) { 88 | id -> Int4, 89 | uname -> Varchar, 90 | psw_hash -> Varchar, 91 | join_at -> Timestamp, 92 | last_seen -> Timestamp, 93 | avatar -> Varchar, 94 | email -> Varchar, 95 | link -> Varchar, 96 | intro -> Text, 97 | location -> Varchar, 98 | nickname -> Varchar, 99 | permission -> Int2, 100 | auth_from -> Varchar, 101 | email_confirmed -> Bool, 102 | karma -> Int4, 103 | is_pro -> Bool, 104 | can_push -> Bool, 105 | push_email -> Varchar, 106 | } 107 | } 108 | 109 | table! { 110 | votecomments (uname, comment_id) { 111 | uname -> Varchar, 112 | comment_id -> Int4, 113 | vote_at -> Timestamp, 114 | vote_as -> Int2, 115 | } 116 | } 117 | 118 | table! { 119 | voteitems (uname, item_id) { 120 | uname -> Varchar, 121 | item_id -> Int4, 122 | vote_at -> Timestamp, 123 | vote_as -> Int2, 124 | } 125 | } 126 | 127 | joinable!(itemcomments -> comments (comment_id)); 128 | joinable!(itemcomments -> items (item_id)); 129 | joinable!(itemlabels -> items (item_id)); 130 | joinable!(votecomments -> comments (comment_id)); 131 | joinable!(voteitems -> items (item_id)); 132 | 133 | allow_tables_to_appear_in_same_query!( 134 | background_jobs, 135 | blogs, 136 | comments, 137 | itemcomments, 138 | itemlabels, 139 | items, 140 | labels, 141 | users, 142 | votecomments, 143 | voteitems, 144 | ); 145 | -------------------------------------------------------------------------------- /src/util/email.rs: -------------------------------------------------------------------------------- 1 | // send confirm email 2 | use crate::errors::ServiceError; 3 | use lettre::file::FileTransport; 4 | use lettre::smtp::authentication::{Credentials, Mechanism}; 5 | use lettre::smtp::SmtpClient; 6 | use lettre::{SendableEmail, Transport}; 7 | use lettre_email::Email; 8 | use std::path::Path; 9 | 10 | #[derive(Debug)] 11 | pub struct MailConfig { 12 | pub smtp_login: String, 13 | pub smtp_password: String, 14 | pub smtp_server: String, 15 | } 16 | 17 | pub fn init_config() -> Option { 18 | match ( 19 | dotenv::var("MAIL_SMTP_LOGIN"), 20 | dotenv::var("MAIL_SMTP_PASSWORD"), 21 | dotenv::var("MAIL_SMTP_SERVER"), 22 | ) { 23 | (Ok(login), Ok(password), Ok(server)) => Some(MailConfig { 24 | smtp_login: login, 25 | smtp_password: password, 26 | smtp_server: server, 27 | }), 28 | _ => None, 29 | } 30 | } 31 | 32 | fn build_email( 33 | recipient: &str, 34 | subject: &str, 35 | body: &str, 36 | mail_config: &Option, 37 | ) -> Result { 38 | let sender = mail_config 39 | .as_ref() 40 | .map(|s| s.smtp_login.as_str()) 41 | .unwrap_or("test@Toplog"); 42 | 43 | let email = Email::builder() 44 | .to(recipient) 45 | .from(sender) 46 | .subject(subject) 47 | .body(body) 48 | .build() 49 | .map_err(|_| ServiceError::BadRequest("Error in Building email".into()))?; 50 | 51 | Ok(email.into()) 52 | } 53 | 54 | fn send_email( 55 | recipient: &str, 56 | subject: &str, 57 | body: &str, 58 | ) -> Result<(), ServiceError> { 59 | let mail_config = init_config(); 60 | let email = build_email(recipient, subject, body, &mail_config)?; 61 | 62 | match mail_config { 63 | Some(mail_config) => { 64 | let mut transport = SmtpClient::new_simple(&mail_config.smtp_server) 65 | .map_err(|_| { 66 | ServiceError::BadRequest("Error in Building email".into()) 67 | })? 68 | .credentials(Credentials::new( 69 | mail_config.smtp_login, 70 | mail_config.smtp_password, 71 | )) 72 | .smtp_utf8(true) 73 | .authentication_mechanism(Mechanism::Plain) 74 | .transport(); 75 | 76 | let result = transport.send(email); 77 | result.map_err(|_| { 78 | ServiceError::BadRequest("Error in sending email".into()) 79 | })?; 80 | } 81 | None => { 82 | let mut sender = FileTransport::new(Path::new("/tmp")); 83 | let result = sender.send(email); 84 | result.map_err(|_| { 85 | ServiceError::BadRequest("Email file could not be generated".into()) 86 | })?; 87 | } 88 | } 89 | 90 | Ok(()) 91 | } 92 | 93 | pub fn try_send_confirm_email( 94 | email: &str, 95 | user_name: &str, 96 | token: &str, 97 | ) -> Result<(), ServiceError> { 98 | let subject = "Please verify your email address"; 99 | use crate::util::helper::en_base64; 100 | let body = format!( 101 | "Hello {}: \n\n Welcome to toplog.cc. Please click or copy to browser the link below to verify your email address. Thank you! \n\n https://toplog.cc/confirm/{} \n\n This link will expire in 48 hours. \n\n\n The Toplog Team", 102 | user_name, en_base64(token) 103 | ); 104 | 105 | send_email(email, subject, &body) 106 | } 107 | 108 | pub fn try_send_reset_email( 109 | email: &str, 110 | user_name: &str, 111 | token: &str, 112 | ) -> Result<(), ServiceError> { 113 | let subject = "Please Reset Your password"; 114 | let body = format!( 115 | "Hello {}: \n\n Someone (hopefully you) requested we reset your password at Toplog. \n\n The Token to reset password as below:\n\n {} \n\n This Token will expire in 2 hours. If not you, just ignore this message. \n\n\n The Toplog Team", 116 | user_name, base64::encode(token) 117 | ); 118 | //println!("reset: {:?}", token); 119 | 120 | send_email(email, subject, &body) 121 | } 122 | 123 | //use crate::bot::jobs::Environment; 124 | use swirl::errors::PerformError; 125 | 126 | // #[swirl::background_job] 127 | // pub fn send_email_back_job( 128 | // env: &Environment, 129 | // ty: String, 130 | // email: String, 131 | // user_name: String, 132 | // token: String 133 | // ) -> Result<(), PerformError> { 134 | // if ty.trim() == "reset" { 135 | // try_send_reset_email(&email, &user_name, &token) 136 | // .map_err(|e|{Box::new(dyn e.into())}) 137 | // } else { 138 | // try_send_confirm_email(&email, &user_name, &token) 139 | // } 140 | // } 141 | 142 | #[cfg(test)] 143 | mod tests { 144 | use super::*; 145 | 146 | #[test] 147 | fn sending_to_invalid_email_fails() { 148 | let result = send_email( 149 | "String.Format(\"{0}.{1}@toplog.cc\", FirstName, LastName)", 150 | "test", 151 | "test", 152 | ); 153 | assert!(result.is_err()); 154 | } 155 | 156 | #[test] 157 | fn sending_to_valid_email_succeeds() { 158 | let result = send_email("****@gmail.com", "test", "test"); 159 | assert!(result.is_ok()); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/util/helper.rs: -------------------------------------------------------------------------------- 1 | // some sharing helpers 2 | 3 | use base64; 4 | use deunicode::deunicode_char; 5 | use regex::Regex; 6 | use std::collections::HashMap; 7 | 8 | lazy_static! { 9 | static ref RE_SPC: Regex = Regex::new(r"[^a-zA-Z0-9]").unwrap(); // let fail in test 10 | } 11 | 12 | // slug, better to show url: ty as type, for rut|item|collect 13 | pub fn gen_slug(text: &str) -> String { 14 | let ts = chrono::Utc::now().timestamp(); 15 | format!("{}-{}", slugify(text), ts) 16 | } 17 | 18 | // credit to https://github.com/Stebalien/slug-rs/blob/master/src/lib.rs 19 | pub fn slugify>(s: S) -> String { 20 | _slugify(s.as_ref()) 21 | } 22 | 23 | fn _slugify(s: &str) -> String { 24 | let mut slug: Vec = Vec::with_capacity(s.len()); 25 | // Starts with true to avoid leading - 26 | let mut prev_is_dash = true; 27 | { 28 | let mut push_char = |x: u8| { 29 | match x { 30 | b'a'..=b'z' | b'0'..=b'9' => { 31 | prev_is_dash = false; 32 | slug.push(x); 33 | } 34 | b'A'..=b'Z' => { 35 | prev_is_dash = false; 36 | slug.push(x - b'A' + b'a'); // u8 37 | } 38 | _ => { 39 | if !prev_is_dash { 40 | slug.push(b'-'); 41 | prev_is_dash = true; 42 | } 43 | } 44 | } 45 | }; 46 | 47 | for c in s.chars() { 48 | if c.is_ascii() { 49 | (push_char)(c as u8); 50 | } else { 51 | for &cx in deunicode_char(c).unwrap_or("-").as_bytes() { 52 | (push_char)(cx); 53 | } 54 | } 55 | } 56 | } 57 | 58 | // It's not really unsafe in practice, we know we have ASCII 59 | let mut string = unsafe { String::from_utf8_unchecked(slug) }; 60 | if string.ends_with('-') { 61 | string.pop(); 62 | } 63 | // We likely reserved more space than needed. 64 | string.shrink_to_fit(); 65 | string 66 | } 67 | 68 | // base64 en_decode 69 | pub fn de_base64(c: &str) -> String { 70 | let s = String::from_utf8( 71 | base64::decode_config(c, base64::URL_SAFE_NO_PAD) 72 | .unwrap_or(Vec::new()) 73 | ) 74 | .unwrap_or("".into()); 75 | s 76 | } 77 | 78 | pub fn en_base64(s: &str) -> String { 79 | let c = base64::encode_config(s, base64::URL_SAFE_NO_PAD); 80 | c 81 | } 82 | 83 | // extract host of url 84 | lazy_static! { 85 | static ref RE_S: Regex = 86 | Regex::new(r"https?://").unwrap(); // let fail in test 87 | } 88 | lazy_static! { 89 | static ref RE_P: Regex = 90 | Regex::new(r"/.*").unwrap(); // let fail in test 91 | } 92 | 93 | pub fn get_host(s: &str) -> String { 94 | let url = RE_S.replace_all(s, ""); 95 | let host = RE_P.replace_all(&url, "").replace("www.", ""); 96 | host 97 | } 98 | 99 | 100 | // serde links vec, save spidered links as json 101 | // 102 | #[derive(Serialize, Deserialize, Default, Debug)] 103 | pub struct SpLinks { pub links: Vec } 104 | pub const LINKS_JSON_DIR: &str = "links.json"; 105 | 106 | pub fn serde_links(links: Vec) { 107 | let sp_links = SpLinks { links: links }; 108 | let ser_sp_links = serde_json::to_string(&sp_links) 109 | .unwrap_or(String::new()); 110 | std::fs::write(LINKS_JSON_DIR, ser_sp_links.as_bytes()).unwrap_or(()); 111 | } 112 | 113 | pub fn deserde_links() -> Vec { 114 | let read_links = String::from_utf8( 115 | std::fs::read(LINKS_JSON_DIR) 116 | .unwrap_or("Not Found".to_owned().into_bytes()), 117 | ) 118 | .unwrap_or_default(); 119 | 120 | let deser_links: SpLinks = serde_json::from_str(&read_links) 121 | .unwrap_or_default(); 122 | 123 | deser_links.links 124 | } 125 | 126 | pub fn serde_add_links(mut add: Vec) { 127 | let read_links = String::from_utf8( 128 | std::fs::read(LINKS_JSON_DIR) 129 | .unwrap_or("Not Found".to_owned().into_bytes()), 130 | ) 131 | .unwrap_or_default(); 132 | 133 | let old_sp_links: SpLinks = serde_json::from_str(&read_links) 134 | .unwrap_or_default(); 135 | 136 | let mut new_links = old_sp_links.links; 137 | new_links.append(&mut add); 138 | 139 | serde_links(new_links) 140 | } 141 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | // util mod 2 | 3 | // some helper 4 | 5 | pub mod email; 6 | pub mod helper; 7 | -------------------------------------------------------------------------------- /src/view/form.rs: -------------------------------------------------------------------------------- 1 | pub use askama::Template; 2 | use actix_web::{ 3 | web::{Data, Path, Query}, 4 | Error, HttpResponse, ResponseError, 5 | Result 6 | }; 7 | use crate::errors::{ServiceError, ServiceResult}; 8 | use crate::api::auth::{CheckAuth, CheckUser, CheckCan, CheckCsrf, generate_token}; 9 | 10 | #[derive(Template)] 11 | #[template(path = "auth_form.html")] 12 | pub struct AuthFormTmpl<'a> { 13 | pub csrf_tok: &'a str, 14 | pub uname: &'a str, 15 | } 16 | 17 | pub async fn auth_form( 18 | auth: CheckAuth, 19 | ) -> Result { 20 | let uname = auth.0; 21 | let tok = generate_token(&uname, "auth@uname", 1*24*3600)?; 22 | let af = AuthFormTmpl { 23 | csrf_tok: &tok, 24 | uname: &uname, 25 | }; 26 | let s = af.render().unwrap_or("Rendering failed".into()); 27 | 28 | Ok(HttpResponse::Ok() 29 | .content_type("text/html; charset=utf-8") 30 | .body(s) 31 | ) 32 | } 33 | 34 | #[derive(Template)] 35 | #[template(path = "0_item_form.html")] 36 | pub struct ItemFormTmpl<'a> { 37 | pub csrf_tok: &'a str, 38 | pub is_new: bool, 39 | } 40 | 41 | // GET /newitem 42 | // 43 | pub async fn new_item( 44 | auth: CheckUser, 45 | ) -> Result { 46 | let tok = generate_token(&auth.uname, "item@new", 1*24*3600)?; 47 | let ns = ItemFormTmpl { 48 | csrf_tok: &tok, 49 | is_new: true, 50 | }; 51 | let s = ns.render().unwrap_or("Rendering failed".into()); 52 | 53 | Ok(HttpResponse::Ok() 54 | .content_type("text/html; charset=utf-8") 55 | .body(s) 56 | ) 57 | } 58 | 59 | // GET /edititem?id= 60 | // 61 | pub async fn edit_item( 62 | auth: CheckUser, 63 | ) -> Result { 64 | let tok = generate_token(&auth.uname, "item@edit", 1*24*3600)?; 65 | let ns = ItemFormTmpl { 66 | csrf_tok: &tok, 67 | is_new: false, 68 | }; 69 | let s = ns.render().unwrap_or("Rendering failed".into()); 70 | 71 | Ok(HttpResponse::Ok() 72 | .content_type("text/html; charset=utf-8") 73 | .body(s) 74 | ) 75 | } 76 | 77 | #[derive(Template)] 78 | #[template(path = "0_blog_form.html")] 79 | pub struct BlogFormTmpl<'a> { 80 | pub csrf_tok: &'a str, 81 | pub is_new: bool, 82 | } 83 | 84 | // GET /newblog 85 | // 86 | pub async fn new_blog( 87 | auth: CheckUser, 88 | ) -> Result { 89 | let tok = generate_token(&auth.uname, "blog@new", 1*24*3600)?; 90 | let ns = BlogFormTmpl { 91 | csrf_tok: &tok, 92 | is_new: true, 93 | }; 94 | let s = ns.render().unwrap_or("Rendering failed".into()); 95 | 96 | Ok(HttpResponse::Ok() 97 | .content_type("text/html; charset=utf-8") 98 | .body(s) 99 | ) 100 | } 101 | 102 | // GET /editblog?id= 103 | // 104 | pub async fn edit_blog( 105 | auth: CheckUser, 106 | ) -> Result { 107 | let tok = generate_token(&auth.uname, "blog@edit", 1*24*3600)?; 108 | let ns = BlogFormTmpl { 109 | csrf_tok: &tok, 110 | is_new: false, 111 | }; 112 | let s = ns.render().unwrap_or("Rendering failed".into()); 113 | 114 | Ok(HttpResponse::Ok() 115 | .content_type("text/html; charset=utf-8") 116 | .body(s) 117 | ) 118 | } 119 | 120 | #[derive(Template)] 121 | #[template(path = "0_submit_form.html")] 122 | pub struct SubmitToTmpl<'a> { 123 | pub csrf_tok: &'a str, 124 | } 125 | 126 | pub async fn submit_to ( 127 | auth: CheckAuth, 128 | ) -> Result { 129 | let tok = generate_token(&auth.0, "submit@item", 1*24*3600)?; 130 | let na = SubmitToTmpl { 131 | csrf_tok: &tok, 132 | }; 133 | let s = na.render().unwrap_or("Rendering failed".into()); 134 | 135 | Ok(HttpResponse::Ok() 136 | .content_type("text/html; charset=utf-8") 137 | .body(s) 138 | ) 139 | } 140 | -------------------------------------------------------------------------------- /src/view/mod.rs: -------------------------------------------------------------------------------- 1 | // view redered by server side 2 | 3 | pub mod tmpl; 4 | pub mod form; 5 | 6 | use regex::Regex; 7 | use std::collections::HashMap; 8 | use unic_segment::GraphemeIndices; 9 | use chrono::NaiveDate; 10 | pub use askama::Template; 11 | 12 | use crate::api::item::{Item}; 13 | use crate::api::blog::{Blog}; 14 | use crate::api::auth::CheckUser; 15 | 16 | 17 | lazy_static! { 18 | pub static ref TOPIC_VEC: Vec<&'static str> = { 19 | vec!( 20 | "all", 21 | "Rust", "Go", "Swift", "TypeScript", "Dart", 22 | "Python", "C-sharp", "C", "CPP", "JavaScript", "Java", "PHP", "Kotlin", "DataBase" 23 | ) 24 | }; 25 | 26 | pub static ref TY_VEC: Vec<&'static str> = { 27 | vec!( 28 | "Article", "Book", "Event", "Job", "Media", "Project", "Translate", "Misc" 29 | ) 30 | }; 31 | } 32 | 33 | 34 | #[derive(Template)] 35 | #[template(path = "collection.html")] 36 | pub struct CollectionTmpl<'a> { 37 | pub ty: &'a str, 38 | pub topic: &'a str, 39 | pub items: &'a Vec, 40 | pub blogs: &'a Vec, 41 | pub tys: &'a Vec<&'a str>, 42 | } 43 | 44 | #[derive(Template)] 45 | #[template(path = "item.html")] 46 | pub struct ItemTmpl<'a> { 47 | pub item: &'a Item, 48 | } 49 | 50 | #[derive(Template)] 51 | #[template(path = "more_item.html")] 52 | pub struct ItemsTmpl<'a> { 53 | pub items: &'a Vec, 54 | pub topic: &'a str, 55 | } 56 | 57 | #[derive(Template)] 58 | #[template(path = "profile.html")] 59 | pub struct ProfileTmpl<'a> { 60 | pub user: &'a CheckUser, 61 | pub is_self: bool, 62 | } 63 | 64 | #[derive(Template)] 65 | #[template(path = "about.html")] 66 | pub struct AboutTmpl(); 67 | 68 | #[derive(Template)] 69 | #[template(path = "sitemap/sitemap.xml")] 70 | pub struct SiteMapTmpl<'a> { 71 | pub tys: &'a Vec<&'a str>, 72 | pub topics: &'a Vec<&'a str>, 73 | pub lastmod: &'a str, 74 | } 75 | 76 | 77 | // ================================================== 78 | // custom filters =================================== 79 | // 80 | mod filters { 81 | use askama::Result as TmplResult; 82 | use askama::Error as TmplError; 83 | use chrono::{ 84 | format::{Item, StrftimeItems}, 85 | DateTime, FixedOffset, NaiveDate, NaiveDateTime, Utc, 86 | }; 87 | use chrono_tz::Tz; 88 | use unic_segment::GraphemeIndices; 89 | use log::error; 90 | 91 | pub fn host(s: &str) -> TmplResult { 92 | use crate::util::helper::get_host; 93 | let s_host = get_host(s); 94 | Ok(s_host) 95 | } 96 | 97 | pub fn num_unit(num: &i32) -> TmplResult { 98 | let x = *num; 99 | let (n, u): (i32, &str) = if x > 9000 { 100 | (9, "k+") 101 | } else if x >= 1000 { 102 | (num / 1000, "k") 103 | } else { 104 | (x, "") 105 | }; 106 | let res = n.to_string() + u; 107 | 108 | Ok(res) 109 | } 110 | 111 | pub fn dt_fmt( 112 | value: &NaiveDateTime, 113 | fmt: &str, 114 | ) -> TmplResult { 115 | 116 | let format: &str = if fmt.len() > 0 { 117 | fmt 118 | } else { 119 | "%Y-%m-%d %R" 120 | }; 121 | // https://docs.rs/chrono/0.4.15/chrono/format/strftime/index.html 122 | let formatted = value.format(format).to_string(); 123 | 124 | Ok(formatted) 125 | } 126 | 127 | pub fn md(s: &str) -> TmplResult { 128 | use pulldown_cmark::{Parser, Options, html::push_html}; 129 | use ammonia::clean; // for HTML Sanitization 130 | let mut options = Options::all(); 131 | // options.insert(Options::ENABLE_TABLES); 132 | // options.insert(Options::ENABLE_FOOTNOTES); 133 | // options.insert(Options::ENABLE_TASKLISTS); 134 | 135 | let parser = Parser::new_ext(s, options); 136 | let mut html_res = String::new(); 137 | push_html(&mut html_res, parser); 138 | let clean_html = clean(&*html_res); 139 | 140 | Ok(clean_html) 141 | } 142 | 143 | pub fn pluralize(num: &i32, m: &str, s: &str) -> TmplResult { 144 | 145 | let res = if (num.abs() - 1).abs() > 0 { 146 | num.to_string() + " " + m + s 147 | } else { 148 | num.to_string() + " " + m 149 | }; 150 | 151 | Ok(res) 152 | } 153 | 154 | pub fn showless(s: &str, len: &usize) -> TmplResult { 155 | 156 | let graphemes = GraphemeIndices::new(s).collect::>(); 157 | let length = graphemes.len(); 158 | let least = *len; 159 | 160 | if least >= length { 161 | return Ok(s.to_string()); 162 | } 163 | 164 | let r_s = s[..graphemes[least].0].to_string(); 165 | 166 | let last_link = r_s.rfind("").unwrap_or(0); 168 | let result = if last_link > last_end_link { 169 | r_s[..graphemes[last_link].0].to_string() 170 | } else { 171 | r_s 172 | }; 173 | 174 | Ok(result) 175 | } 176 | 177 | // base64 encode 178 | pub fn b64_encode(value: &str) -> TmplResult { 179 | use crate::util::helper::en_base64; 180 | let b64 = en_base64(value); 181 | 182 | Ok(b64) 183 | } 184 | 185 | pub fn date_fmt( 186 | value: &NaiveDate, 187 | fmt: &str, 188 | ) -> TmplResult { 189 | 190 | let format: &str = if fmt.len() > 0 { 191 | fmt 192 | } else { 193 | "%Y-%m-%d" 194 | }; 195 | 196 | let item = StrftimeItems::new(format); 197 | let formatted = format!("{}", value.format_with_items(item)); 198 | 199 | Ok(formatted) 200 | } 201 | 202 | pub fn datetime_fmt( 203 | value: &dyn std::fmt::Display, 204 | fmt: &str, 205 | tz: &str, 206 | ) -> TmplResult { 207 | let value = value.to_string(); 208 | 209 | let format: &str = if fmt.len() > 0 { 210 | fmt 211 | } else { 212 | "%Y-%m-%d" 213 | }; 214 | 215 | let items: Vec = StrftimeItems::new(format) 216 | .filter(|item| match item { 217 | Item::Error => true, 218 | _ => false, 219 | }) 220 | .collect(); 221 | if !items.is_empty() { return Ok(value) } 222 | 223 | let timezone = if tz.len() > 0 { 224 | match tz.parse::() { 225 | Ok(timezone) => Some(timezone), 226 | Err(e) => { error!("{}", e); return Ok(value) } 227 | } 228 | } else { 229 | None 230 | }; 231 | 232 | let formatted = 233 | if value.contains('T') { 234 | match value.parse::>() { 235 | Ok(val) => match timezone { 236 | Some(timezone) => 237 | val.with_timezone(&timezone).format(format), 238 | None => val.format(format), 239 | }, 240 | Err(_) => match value.parse::() { 241 | Ok(val) => val.format(format), 242 | Err(e) => { error!("{}", e); return Ok(value) } 243 | }, 244 | } 245 | } else { 246 | match NaiveDate::parse_from_str(&value, "%Y-%m-%d") { 247 | Ok(val) => DateTime::::from_utc( 248 | val.and_hms(0, 0, 0), Utc 249 | ) 250 | .format(format), 251 | Err(e) => { error!("{}", e); return Ok(value) } 252 | } 253 | }; 254 | 255 | Ok(formatted.to_string()) 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /static/0_auth.js: -------------------------------------------------------------------------------- 1 | 2 | let RedirectURL = '/'; 3 | document.addEventListener('DOMContentLoaded', async function() { 4 | if (getCookie(TOK)) { 5 | let authBox = document.getElementById('auth-box'); 6 | if (authBox) { authBox.style.display = 'none'; } 7 | } 8 | let query = document.location.search; 9 | let docRefer = document.referrer; 10 | // for do some check: 1-should be same host, 11 | let urlHost = document.location.origin; 12 | 13 | RedirectURL = getRedirect('redirect', query) 14 | || (docRefer.startsWith(urlHost) ? docRefer : '/'); 15 | let toWhat = getQueryParam('to', query); 16 | let toNum = toWhat == 'signin' 17 | ? 0 18 | : toWhat == 'signup' 19 | ? 1 20 | : toWhat == 'reset' 21 | ? 2 22 | : toWhat == 'changepsw' 23 | ? 3 24 | : toWhat == 'update' ? 4 : 5; 25 | 26 | // redirect when authed on signin / signup 27 | if (toNum < 2 && getCookie(TOK)) { 28 | window.location.href = RedirectURL.search(/auth\?to=sign/) == -1 ? RedirectURL : '/'; 29 | } 30 | 31 | if (toNum >= 3 && !getCookie(TOK)) return; 32 | if (toNum == 4) { 33 | let name = document.getElementById('hide-uname'); 34 | if (!name) return; 35 | let uname = name.value; 36 | if (uname != getCookie(IDENT)) return; 37 | // set user 38 | let getUser = await fetch(`/api/users/${uname}`); 39 | if (!getUser.ok) return; 40 | let userRes = await getUser.json(); 41 | let userInfo = Object.assign({ },userRes.user); 42 | // console.log(userInfo); 43 | let ids = ['nickname', 'avatar', 'email', 'location', 'intro']; 44 | setValsByIDs(ids, 'auth-up-', userInfo); 45 | } 46 | 47 | onSwitch(toNum); 48 | let titl = document.getElementById('auth-form-title'); 49 | if (titl) { 50 | titl.innerText = toNum < 2 51 | ? 'Welcome' 52 | : toNum == 2 53 | ? 'Recover Password' 54 | : toNum == 3 55 | ? 'Change Password' 56 | : 'Update Profile'; 57 | } 58 | }, false) 59 | 60 | async function login() { 61 | let ids = ['username', 'psw']; 62 | let loginfo = getValsByIDs(ids, 'auth-'); 63 | let uname = loginfo[ids.indexOf('username')]; 64 | let password = loginfo[ids.indexOf('psw')]; 65 | if (uname.trim().length < 3 || password.length < 8 ) { 66 | alert('Invalid Input') 67 | } 68 | let authData = { 69 | uname: uname.trim(), 70 | password: Base64encode(password, true) 71 | }; 72 | let options = { 73 | method: 'POST', 74 | headers: {'Content-Type': 'application/json'}, 75 | body: JSON.stringify(authData) 76 | }; 77 | let authResp = await fetch('/api/signin', options); 78 | if (!authResp.ok) { alert("Failed.."); return } 79 | 80 | let auth = await authResp.json(); 81 | setAuth(auth); 82 | window.location.href = RedirectURL; 83 | } 84 | 85 | async function signup() { 86 | let ids = ['newuser', 'newpsw', 'repsw']; 87 | let loginfo = getValsByIDs(ids, 'auth-'); 88 | let uname = loginfo[ids.indexOf('newuser')]; 89 | let password = loginfo[ids.indexOf('newpsw')]; 90 | let confirm = loginfo[ids.indexOf('repsw')]; 91 | if (!regName.test(uname.trim()) || !regPsw.test(password)) { 92 | alert('Invalid Input'); 93 | return 94 | } 95 | if (password !== confirm) { alert('Password Not Match'); return} 96 | 97 | let authData = { 98 | uname: uname.trim(), 99 | email: '', 100 | password: Base64encode(password, true), 101 | confirm: Base64encode(password, true), 102 | agree: true, 103 | }; 104 | let options = { 105 | method: 'POST', 106 | headers: {'Content-Type': 'application/json'}, 107 | body: JSON.stringify(authData) 108 | }; 109 | let regResp = await fetch('/api/signup', options); 110 | if (!regResp.ok) { alert("Failed.."); return } 111 | window.location.href = '/auth?to=signin'; 112 | } 113 | 114 | function reset() { /*TODO*/ } 115 | 116 | async function changePsw() { 117 | if (!getCookie(TOK)) return; 118 | let ids = ['oldpsw', 'chpsw', 'rechpsw']; 119 | let info = getValsByIDs(ids, 'auth-'); 120 | let oldPsw = info[ids.indexOf('oldpsw')]; 121 | let newPsw = info[ids.indexOf('chpsw')]; 122 | let confirm = info[ids.indexOf('rechpsw')]; 123 | if (!regPsw.test(oldPsw) || !regPsw.test(newPsw) || newPsw != confirm) { 124 | alert('Invalid Input'); 125 | return 126 | } 127 | let uname = getCookie(IDENT); 128 | let pswData = { 129 | old_psw: Base64encode(oldPsw, true), 130 | new_psw: Base64encode(newPsw, true), 131 | uname, 132 | }; 133 | let options = { 134 | method: 'PUT', 135 | headers: { 136 | 'Content-Type': 'application/json', 137 | 'Authorization': getCookie(TOK), 138 | }, 139 | body: JSON.stringify(pswData) 140 | }; 141 | let chResp = await fetch('/api/users/' + uname, options); 142 | if (!chResp.ok) { alert("Failed.."); return } 143 | signOut('/auth?to=signin'); 144 | } 145 | 146 | const regPsw = /^[\w#@~%^$&*-]{8,18}$/; 147 | //const regPsw = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[#@!~%^$&*-])[a-zA-Z\d#@!~%^$&*-]{8,18}$/; 148 | const regEmail = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; 149 | const regName = /^[\w-]{3,16}$/; 150 | 151 | let trigger = 0; // 0- login, 1- signup, 2- reset 3- change psw 4- update 152 | function onSwitch(t=0) { 153 | trigger = t; 154 | let signupForm = document.getElementById('sign-up-form'); 155 | let signinForm = document.getElementById('sign-in-form'); 156 | let resetForm = document.getElementById('reset-form'); 157 | let chpswForm = document.getElementById('chpsw-form'); 158 | let upForm = document.getElementById('update-form'); 159 | if (signinForm) { signinForm.style.display = trigger == 0 ? '' : 'none'; } 160 | if (signupForm) { signupForm.style.display = trigger == 1 ? '' : 'none'; } 161 | if (resetForm) { resetForm.style.display = trigger == 2 ? '' : 'none'; } 162 | if (chpswForm) { chpswForm.style.display = trigger == 3 ? '' : 'none'; } 163 | if (upForm) { upForm.style.display = trigger == 4 ? '' : 'none'; } 164 | } 165 | 166 | function setAuth(reg) { 167 | setCookie(TOK, reg.token, {expires: reg.exp}); 168 | setCookie(IDENT, reg.user.uname, {expires: reg.exp}); 169 | //setCookie(CAN, reg.user.permission, {expires: reg.exp}); 170 | setCookie('oMg', reg.omg, {expires: reg.exp}); 171 | } 172 | 173 | async function updateUser() { 174 | if (!getCookie(TOK)) return; 175 | let ids = ['nickname', 'avatar', 'email', 'location', 'intro']; 176 | let info = getValsByIDs(ids, 'auth-up-'); 177 | let uname = getCookie(IDENT); 178 | let upUser = { 179 | uname, 180 | avatar: info[ids.indexOf('avatar')], 181 | email: info[ids.indexOf('email')], 182 | intro: info[ids.indexOf('intro')], 183 | location: info[ids.indexOf('location')], 184 | nickname: info[ids.indexOf('nickname')], 185 | }; 186 | let options = { 187 | method: 'POST', 188 | headers: { 189 | 'Content-Type': 'application/json', 190 | 'Authorization': getCookie(TOK), 191 | }, 192 | body: JSON.stringify(upUser) 193 | }; 194 | let upResp = await fetch(`/api/users/${uname}`, options); 195 | if (!upResp.ok) return; 196 | 197 | window.location.href = '/@' + uname; 198 | } 199 | -------------------------------------------------------------------------------- /static/0_blog.js: -------------------------------------------------------------------------------- 1 | let S_PREFIX = "new-b-"; 2 | let UP_BLOG = {aname: '', intro: '', blog_link: ''}; 3 | 4 | let IS_NEW_OR_NOT = true; // new or edit 5 | 6 | function buildBlog() { 7 | let ids = [ 8 | 'aname', 'avatar', 'intro', 'topic', 'blog_link','blog_host', 9 | 'gh_link', 'other_link', 'csrf' 10 | ]; 11 | let vals = getValsByIDs(ids, S_PREFIX); 12 | 13 | let aname = vals[ids.indexOf('aname')]; 14 | let avatar = vals[ids.indexOf('avatar')]; 15 | let intro = vals[ids.indexOf('intro')]; 16 | let topic = vals[ids.indexOf('topic')]; 17 | let blog_host = vals[ids.indexOf('blog_host')]; 18 | let blog_link = vals[ids.indexOf('blog_link')]; 19 | let gh_link = vals[ids.indexOf('gh_link')]; 20 | let other_link = vals[ids.indexOf('other_link')]; 21 | let csrf = vals[ids.indexOf('csrf')]; 22 | 23 | let ckb = document.getElementById('new-b-is_top'); 24 | // console.log(ckb.checked) 25 | let is_top = ckb && ckb.checked; 26 | 27 | // refer to struct NewBlog 28 | let new_blog = { 29 | aname, 30 | avatar, 31 | intro, 32 | topic, 33 | blog_link, 34 | blog_host, 35 | gh_link, 36 | other_link, 37 | is_top, 38 | }; 39 | // console.log(new_blog); 40 | return [new_blog, csrf]; 41 | } 42 | 43 | async function newBlog() { 44 | if (!getCookie(TOK)) return; 45 | let bd = buildBlog(); 46 | let new_blg = bd[0]; 47 | let csrf = bd[1]; 48 | let aname = new_blg.aname; 49 | let blogLink = new_blg.blog_link; 50 | 51 | 52 | if (aname && blogLink && csrf) { 53 | let newaBtn = document.getElementById(S_PREFIX + "btn"); 54 | if (newaBtn) { newaBtn.innerHTML = "Processing"; } 55 | 56 | // if update, check the change 57 | let IF_TO_FETCH = true; 58 | if (!IS_NEW_OR_NOT 59 | && UP_BLOG.aname == aname 60 | && UP_BLOG.avatar == new_blg.avatar 61 | && UP_BLOG.intro == new_blg.intro 62 | && UP_BLOG.blog_link == new_blg.blog_link 63 | && UP_BLOG.blog_host == new_blg.blog_host 64 | && UP_BLOG.gh_link == new_blg.gh_link 65 | && UP_BLOG.other_link == new_blg.other_link 66 | && UP_BLOG.topic == new_blg.topic 67 | && UP_BLOG.is_top == new_blg.is_top 68 | ) { 69 | IF_TO_FETCH = false; 70 | if (delTagList.length == 0 && addTagList.length == 0) { 71 | alert('Nothing Changed'); 72 | return; 73 | } 74 | } 75 | 76 | let q_data = IS_NEW_OR_NOT 77 | ? new_blg 78 | : Object.assign({id: UP_BLOG.id}, new_blg); 79 | 80 | let headers = { 81 | 'Authorization': getCookie(TOK), 82 | 'CsrfToken': csrf, 83 | 'Content-Type': 'application/json' 84 | }; 85 | 86 | let options = { 87 | method: IS_NEW_OR_NOT ? 'POST' : 'PUT', 88 | headers, 89 | body: JSON.stringify(q_data) 90 | }; 91 | let resp = IF_TO_FETCH 92 | ? await fetch('/api/blogs', options) 93 | : {ok: true, nofetch: true}; 94 | // console.log(resp); 95 | 96 | if (!resp.ok) { 97 | alert("Something failed"); 98 | return; 99 | } 100 | // console.log(resp); 101 | let res_blog = resp.nofetch ? {} : await resp.json(); 102 | 103 | // edit tag 104 | /* if (addTagList.length > 0) { 105 | let addTags = { 106 | tnames: addTagList, 107 | blog_id: res_blog.id || UP_BLOG.id, 108 | method: 1, 109 | }; 110 | await fetch('/api/topics', 111 | { 112 | method: 'PATCH', 113 | headers, 114 | body: JSON.stringify(addTags) 115 | } 116 | ); 117 | } 118 | if (delTagList.length > 0) { 119 | let delTags = { 120 | tnames: delTagList, 121 | blog_id: res_blog.id || UP_BLOG.id, 122 | method: 0, 123 | }; 124 | await fetch('/api/topics', 125 | { 126 | method: 'PATCH', 127 | headers, 128 | body: JSON.stringify(delTags) 129 | } 130 | ); 131 | } */ 132 | 133 | let name = res_blog.aname; 134 | let b64_name = Base64encode(name, true); 135 | window.location.href = '/from?by=' + b64_name; 136 | } else { 137 | alert("Invalid Input", aname, blogLink, csrf); 138 | console.log("Invalid Input", aname, blogLink, csrf); 139 | return; 140 | } 141 | } 142 | 143 | document.addEventListener('DOMContentLoaded', async function() { 144 | if (!getCookie(TOK)) return; 145 | let docPath = document.location.pathname; 146 | if (docPath.startsWith('/editblog')) { 147 | // load blog 148 | let bid = document.location.search.split('?id=')[1]; 149 | if (!bid) return; 150 | let resp = await fetch(`/api/blogs/${bid}`); 151 | if (!resp.ok) return; 152 | let res_blog = await resp.json(); 153 | 154 | UP_BLOG = res_blog; 155 | IS_NEW_OR_NOT = false; 156 | 157 | let ids = [ 158 | 'aname', 'avatar', 'intro', 'topic', 'blog_link', 159 | 'blog_host', 'gh_link', 'other_link' 160 | ]; 161 | setValsByIDs(ids, S_PREFIX, res_blog); 162 | // init checkbox 163 | if (res_blog.is_top) { 164 | let ckbox = document.getElementById('new-b-is_top'); 165 | if (ckbox) { ckbox.setAttribute('checked', true); } 166 | } 167 | 168 | // load tags and init tagsbar 169 | // await loadTagsInitBar('blog', res_blog.id); 170 | } 171 | 172 | initAutoSize(['new-b-intro', 'new-b-avatar']); 173 | 174 | }) 175 | -------------------------------------------------------------------------------- /static/0_item.js: -------------------------------------------------------------------------------- 1 | let S_PREFIX = "new-i-"; 2 | let NEW_AS = ''; 3 | let NEW_FOR = ''; 4 | let UP_ITEM = {title: '', content: '', logo: ''}; 5 | 6 | let IS_NEW_OR_NOT = true; // new or edit 7 | 8 | function buildItem () { 9 | let ids = ['title', 'content', 'logo', 'author', 'ty','topic', 'link', 'pub_at', 'csrf']; 10 | let vals = getValsByIDs(ids, S_PREFIX); 11 | 12 | let title = vals[ids.indexOf('title')]; 13 | let content = vals[ids.indexOf('content')]; 14 | let topic = vals[ids.indexOf('topic')] || NEW_FOR; 15 | let ty = vals[ids.indexOf('ty')] || NEW_AS || 'Article'; 16 | let logo = vals[ids.indexOf('logo')]; 17 | let author = vals[ids.indexOf('author')]; 18 | let link = vals[ids.indexOf('link')]; 19 | let pub_at = vals[ids.indexOf('pub_at')] || getToday(); 20 | let csrf = vals[ids.indexOf('csrf')]; 21 | 22 | // refer to struct NewItem 23 | let new_item = { 24 | title, 25 | content, 26 | logo, 27 | author, 28 | ty, 29 | topic, 30 | link, 31 | post_by: getCookie(IDENT), 32 | pub_at 33 | }; 34 | // console.log(new_item); 35 | return [new_item, csrf]; 36 | } 37 | 38 | async function newItem() { 39 | if (!getCookie(TOK)) return; 40 | let bd = buildItem(); 41 | let new_itm = bd[0]; 42 | let csrf = bd[1]; 43 | let title = new_itm.title; 44 | let topic = new_itm.topic; 45 | let content = new_itm.content; 46 | 47 | if (title && topic && content && csrf) { 48 | let newaBtn = document.getElementById(S_PREFIX + "btn"); 49 | if (newaBtn) { newaBtn.innerHTML = "Processing"; } 50 | 51 | // if update, check the change 52 | let IF_TO_FETCH = true; 53 | if (!IS_NEW_OR_NOT 54 | && UP_ITEM.title == title 55 | && UP_ITEM.content == content 56 | && UP_ITEM.logo == new_itm.logo 57 | && UP_ITEM.author == new_itm.author 58 | && UP_ITEM.topic == topic 59 | && UP_ITEM.ty == new_itm.ty 60 | && UP_ITEM.link == new_itm.link 61 | && UP_ITEM.pub_at == new_itm.pub_at 62 | ) { 63 | IF_TO_FETCH = false; 64 | if (delTagList.length == 0 && addTagList.length == 0) { 65 | alert('Nothing Changed'); 66 | return; 67 | } 68 | } 69 | 70 | let q_data = IS_NEW_OR_NOT 71 | ? new_itm 72 | : Object.assign({id: UP_ITEM.id}, new_itm); 73 | 74 | let headers = { 75 | 'Authorization': getCookie(TOK), 76 | 'CsrfToken': csrf, 77 | 'Content-Type': 'application/json' 78 | }; 79 | 80 | let options = { 81 | method: IS_NEW_OR_NOT ? 'POST' : 'PUT', 82 | headers, 83 | body: JSON.stringify(q_data) 84 | }; 85 | let resp = IF_TO_FETCH 86 | ? await fetch('/api/items', options) 87 | : {ok: true, nofetch: true}; 88 | // console.log(resp); 89 | 90 | if (!resp.ok) { 91 | alert("Something failed"); 92 | return; 93 | } 94 | // console.log(resp); 95 | let res_item = resp.nofetch ? {} : await resp.json(); 96 | 97 | // edit tag 98 | /* if (addTagList.length > 0) { 99 | let addTags = { 100 | tnames: addTagList, 101 | item_id: res_item.id || UP_ITEM.id, 102 | method: 1, 103 | }; 104 | await fetch('/api/topics', 105 | { 106 | method: 'PATCH', 107 | headers, 108 | body: JSON.stringify(addTags) 109 | } 110 | ); 111 | } 112 | if (delTagList.length > 0) { 113 | let delTags = { 114 | tnames: delTagList, 115 | item_id: res_item.id || UP_ITEM.id, 116 | method: 0, 117 | }; 118 | await fetch('/api/topics', 119 | { 120 | method: 'PATCH', 121 | headers, 122 | body: JSON.stringify(delTags) 123 | } 124 | ); 125 | } */ 126 | 127 | let itmid = res_item.id || UP_ITEM.id; 128 | if (!itmid) return; 129 | window.location.href = '/item/' + itmid; 130 | } else { 131 | alert("Invalid Input"); 132 | return; 133 | } 134 | } 135 | 136 | async function newItemViaUrl() { 137 | if (!getCookie(TOK)) return; 138 | let urlBtn = document.getElementById('new-i-url-btn'); 139 | if (urlBtn) { urlBtn.innerHTML = "Processing"; } 140 | 141 | let urlEle = document.getElementById('new-i-viaurl'); 142 | let url = urlEle ? urlEle.value : ''; 143 | if (url.length < 1) return; 144 | 145 | let csrfTok = document.getElementById('new-i-csrf'); 146 | let csrf = csrfTok ? csrfTok.value : '' 147 | if (!csrf) return; 148 | 149 | let sp_data = { 150 | url, 151 | topic: NEW_FOR || '', 152 | ty: NEW_AS || '' 153 | }; 154 | 155 | let options = { 156 | method: 'PUT', 157 | headers: { 158 | 'Authorization': getCookie(TOK), 159 | 'CsrfToken': csrf, 160 | 'Content-Type': 'application/json' 161 | }, 162 | body: JSON.stringify(sp_data) 163 | }; 164 | let resp = await fetch('/api/spider', options); 165 | 166 | if (!resp.ok) { 167 | alert("Something failed"); 168 | return; 169 | } 170 | // console.log(resp); 171 | let res_item = await resp.json(); 172 | window.location.href = '/item/' + res_item.id; 173 | } 174 | 175 | // hide or show viaurl 176 | function switchForm(to=0) { 177 | let viaurl = document.getElementById('new-i-form-viaurl'); 178 | let viaform = document.getElementById('new-i-form-page'); 179 | if (viaurl && viaform) { 180 | viaurl.style.display = to ? 'none' : ''; 181 | viaform.style.display = to ? '' : 'none'; 182 | } 183 | } 184 | 185 | document.addEventListener('DOMContentLoaded', async function() { 186 | if (!getCookie(TOK)) return; 187 | let urlPath = document.location.pathname; 188 | let urlQry = document.location.search; 189 | if (urlPath.startsWith('/edititem')) { 190 | // load item 191 | let itmid = urlQry.split('?id=')[1]; 192 | if (!itmid) return; 193 | let resp = await fetch(`/api/items/${itmid}`); 194 | if (!resp.ok) return; 195 | let res_item = await resp.json(); 196 | 197 | UP_ITEM = res_item; 198 | IS_NEW_OR_NOT = false; 199 | 200 | let ids = ['title', 'content', 'logo', 'author', 'ty','topic', 'link', 'pub_at']; 201 | setValsByIDs(ids, S_PREFIX, res_item); 202 | 203 | initAutoSize(['new-i-content', 'new-i-link', 'new-i-logo']); 204 | 205 | // load tags and init tagsbar 206 | // await loadTagsInitBar('item', res_item.id); 207 | } else { // newitem 208 | let viaform = document.getElementById('new-i-form-page'); 209 | if (viaform) { viaform.style.display = 'none'; } 210 | 211 | NEW_AS = getQueryParam('as', urlQry) || ''; 212 | NEW_FOR = getQueryParam('for', urlQry) || ''; 213 | // console.log(NEW_AS, NEW_FOR); 214 | let newids = ['ty','topic']; 215 | setValsByIDs(newids, S_PREFIX, {ty: NEW_AS, topic: NEW_FOR}); 216 | } 217 | }) 218 | -------------------------------------------------------------------------------- /static/0_submit.js: -------------------------------------------------------------------------------- 1 | let SUB_PREFIX = "subtl-"; 2 | 3 | function buildSubmit() { 4 | let ids = [ 5 | 'title','link','content','pub_at','author','topic','ty','logo','csrf' 6 | ]; 7 | let vals = getValsByIDs(ids, SUB_PREFIX); 8 | let csrf = vals[ids.indexOf('csrf')]; 9 | 10 | // refer to struct 11 | let newSub = { 12 | title: vals[ids.indexOf('title')], 13 | content: vals[ids.indexOf('content')], 14 | logo: vals[ids.indexOf('logo')], 15 | author: vals[ids.indexOf('author')], 16 | ty: vals[ids.indexOf('ty')], 17 | topic: vals[ids.indexOf('topic')], 18 | link: vals[ids.indexOf('link')], 19 | post_by: getCookie(IDENT), 20 | pub_at: vals[ids.indexOf('pub_at')], 21 | }; 22 | return [newSub, csrf]; 23 | } 24 | 25 | async function onSubmit() { 26 | if (!getCookie(TOK)) return; 27 | const bd = buildSubmit(); 28 | let newSub = bd[0]; 29 | let csrf = bd[1]; 30 | let title = newSub.title; 31 | let topic = newSub.topic; 32 | let content = newSub.content; 33 | 34 | if (!title || !topic || !content || !csrf) return; 35 | 36 | let headers = { 37 | 'Authorization': getCookie(TOK), 38 | 'CsrfToken': csrf, 39 | 'Content-Type': 'application/json' 40 | }; 41 | let options = { 42 | method: 'POST', 43 | headers, 44 | body: JSON.stringify(newSub) 45 | }; 46 | let resp = await fetch('/api/items', options); 47 | if (!resp.ok) { 48 | alert("Something failed"); 49 | return; 50 | } 51 | // console.log(resp); 52 | let res_item = await resp.json(); 53 | let itmid = res_item.id || UP_ITEM.id; 54 | if (!itmid) return; 55 | window.location.href = '/item/' + itmid; 56 | } 57 | 58 | document.addEventListener('DOMContentLoaded', async function() { 59 | if (!getCookie(TOK)) { 60 | let redirUrl = document.location.href; 61 | window.location.href = '/auth?to=signin&redirect=' + redirUrl; 62 | } 63 | // load information in query 64 | let query = decodeURIComponent(document.location.search); 65 | let url = getQueryParam('l', query); 66 | let title = getQueryParam('t', query); 67 | let imgUrl = getQueryParam('img', query); 68 | let desc = getQueryParam('des', query); 69 | let pubat = getQueryParam('d', query) || getToday(); 70 | 71 | let info = { 72 | title: title, 73 | link: url, 74 | logo: imgUrl, 75 | content: desc, 76 | pub_at: pubat, 77 | }; 78 | 79 | initAutoSize(['subtl-title', 'subtl-link', 'subtl-content', 'subtl-logo']); 80 | 81 | let ids = ['title', 'link', 'content', 'pub_at', 'logo']; 82 | setValsByIDs(ids, SUB_PREFIX, info); 83 | }) 84 | -------------------------------------------------------------------------------- /static/0_tagbar.js: -------------------------------------------------------------------------------- 1 | // load tags and init tag bar 2 | // 3 | let showTagList = []; 4 | let addTagList = []; // new added 5 | let delTagList = []; // to be deled tags 6 | 7 | function addTag(e, id) { 8 | if(e.keyCode === 13){ 9 | e.preventDefault(); 10 | let tagInput = document.getElementById(id); 11 | let tagName = tagInput ? tagInput.value : ''; 12 | if (tagName.length > 0 && showTagList.indexOf(tagName) == -1) { 13 | showTagList.push(tagName); 14 | // console.log(showTagList); 15 | tagInput.value = ''; 16 | }; 17 | if (tagName.length > 0 && addTagList.indexOf(tagName) == -1) { 18 | addTagList.push(tagName); 19 | // console.log(addTagList); 20 | tagInput.value = ''; 21 | }; 22 | 23 | initTagBar(showTagList); 24 | } 25 | } 26 | 27 | function delTag(tag) { 28 | showTagList.splice(showTagList.indexOf(tag), 1); 29 | // console.log(showTagList); 30 | if (tag.length > 0 && delTagList.indexOf(tag) == -1) { 31 | delTagList.push(tag); 32 | // console.log(delTagList); 33 | }; 34 | initTagBar(showTagList); 35 | } 36 | 37 | function initTagBar(tags) { 38 | let container = document.getElementById('sa-tags-container'); 39 | container.innerHTML = ''; 40 | 41 | tags.forEach(tagName => { 42 | // add html element 43 | let tagSpan = document.createElement('span'); 44 | tagSpan.className = "new-form-tag"; 45 | tagSpan.innerHTML = `${tagName}`; 46 | let tagButton = document.createElement('a'); 47 | tagButton.className = "edit-tag-btn"; 48 | tagButton.innerHTML = " x"; 49 | tagButton.href = 'javascript:void(0);'; 50 | tagButton.onclick = () => delTag(tagName); 51 | tagSpan.appendChild(tagButton); 52 | container.appendChild(tagSpan); 53 | }) 54 | } 55 | 56 | // async function loadTagsInitBar(pper, id) { 57 | // let resp_tags = await fetch(`/api/topics/${pper}?per=${id}&ext=0&page=1&perpage=${PerPage}`) 58 | // let res_tags = await resp_tags.json(); 59 | // let tags = res_tags.topics.map(tpc => tpc.tname) 60 | // showTagList.push(...tags); 61 | // initTagBar(showTagList); 62 | // } 63 | -------------------------------------------------------------------------------- /static/base64.js: -------------------------------------------------------------------------------- 1 | // base64 js: https://github.com/dankogai/js-base64/blob/master/base64.mjs 2 | // 3 | const _hasbtoa = typeof btoa === 'function'; 4 | const _hasBuffer = typeof Buffer === 'function'; 5 | const _TE = typeof TextEncoder === 'function' ? new TextEncoder() : undefined; 6 | const b64ch = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 7 | const b64chs = [...b64ch]; 8 | 9 | const _fromCC = String.fromCharCode.bind(String); 10 | 11 | const _mkUriSafe = (src) => src 12 | .replace(/[+\/]/g, (m0) => m0 == '+' ? '-' : '_') 13 | .replace(/=+$/m, ''); 14 | 15 | const btoaPolyfill = (bin) => { 16 | // console.log('polyfilled'); 17 | let u32, c0, c1, c2, asc = ''; 18 | const pad = bin.length % 3; 19 | for (let i = 0; i < bin.length;) { 20 | if ((c0 = bin.charCodeAt(i++)) > 255 || 21 | (c1 = bin.charCodeAt(i++)) > 255 || 22 | (c2 = bin.charCodeAt(i++)) > 255) 23 | throw new TypeError('invalid character found'); 24 | u32 = (c0 << 16) | (c1 << 8) | c2; 25 | asc += b64chs[u32 >> 18 & 63] 26 | + b64chs[u32 >> 12 & 63] 27 | + b64chs[u32 >> 6 & 63] 28 | + b64chs[u32 & 63]; 29 | } 30 | return pad ? asc.slice(0, pad - 3) + "===".substring(pad) : asc; 31 | }; 32 | 33 | const _btoa = _hasbtoa 34 | ? (bin) => btoa(bin) 35 | : _hasBuffer 36 | ? (bin) => Buffer.from(bin, 'binary').toString('base64') 37 | : btoaPolyfill; 38 | 39 | const _fromUint8Array = _hasBuffer 40 | ? (u8a) => Buffer.from(u8a).toString('base64') 41 | : (u8a) => { 42 | // cf. https://stackoverflow.com/questions/12710001/how-to-convert-uint8-array-to-base64-encoded-string/12713326#12713326 43 | const maxargs = 0x1000; 44 | let strs = []; 45 | for (let i = 0, l = u8a.length; i < l; i += maxargs) { 46 | strs.push(_fromCC.apply(null, u8a.subarray(i, i + maxargs))); 47 | } 48 | return _btoa(strs.join('')); 49 | }; 50 | 51 | // converts a Uint8Array to a Base64 string. 52 | const fromUint8Array = (u8a, urlsafe = false) => urlsafe 53 | ? _mkUriSafe(_fromUint8Array(u8a)) 54 | : _fromUint8Array(u8a); 55 | // This trick is found broken https://github.com/dankogai/js-base64/issues/130 56 | // const utob = (src: string) => unescape(encodeURIComponent(src)); 57 | // reverting good old fationed regexp 58 | const cb_utob = (c) => { 59 | if (c.length < 2) { 60 | var cc = c.charCodeAt(0); 61 | return cc < 0x80 62 | ? c 63 | : cc < 0x800 64 | ? (_fromCC(0xc0 | (cc >>> 6)) + _fromCC(0x80 | (cc & 0x3f))) 65 | : (_fromCC(0xe0 | ((cc >>> 12) & 0x0f)) 66 | + _fromCC(0x80 | ((cc >>> 6) & 0x3f)) 67 | + _fromCC(0x80 | (cc & 0x3f))); 68 | } 69 | else { 70 | var cc = 0x10000 71 | + (c.charCodeAt(0) - 0xD800) * 0x400 72 | + (c.charCodeAt(1) - 0xDC00); 73 | return (_fromCC(0xf0 | ((cc >>> 18) & 0x07)) 74 | + _fromCC(0x80 | ((cc >>> 12) & 0x3f)) 75 | + _fromCC(0x80 | ((cc >>> 6) & 0x3f)) 76 | + _fromCC(0x80 | (cc & 0x3f))); 77 | } 78 | }; 79 | const re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g; 80 | const utob = (u) => u.replace(re_utob, cb_utob); 81 | const _encode = _hasBuffer 82 | ? (s) => Buffer.from(s, 'utf8').toString('base64') 83 | : _TE 84 | ? (s) => _fromUint8Array(_TE.encode(s)) 85 | : (s) => _btoa(utob(s)); 86 | 87 | const Base64encode = (src, urlsafe = false) => urlsafe 88 | ? _mkUriSafe(_encode(src)) 89 | : _encode(src); 90 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danloh/toplog/de238a2c5012a342a9fcff47a85bcc80dbdd25fb/static/favicon.ico -------------------------------------------------------------------------------- /static/logo/Angular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /static/logo/CPP.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 11 | 12 | 15 | 16 | 17 | 19 | 20 | 21 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /static/logo/Dart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 10 | 18 | 24 | 25 | 27 | 29 | 30 | 31 | 32 | 34 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /static/logo/Go.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/logo/Java.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | ]> 7 | 9 | 10 | 13 | 16 | 19 | 22 | 25 | 28 | 31 | 34 | 37 | 42 | 45 | 50 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /static/logo/JavaScript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/logo/Kotlin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /static/logo/PHP.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Official PHP Logo 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | Official PHP Logo 10 | 11 | 12 | Colin Viebrock 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Copyright Colin Viebrock 1997 - All rights reserved. 25 | 26 | 27 | 1997 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /static/logo/Python.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 25 | 29 | 33 | 34 | 43 | 46 | 50 | 54 | 55 | 64 | 65 | 83 | 85 | 86 | 88 | image/svg+xml 89 | 91 | 92 | 93 | 94 | 99 | 102 | 106 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /static/logo/React.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /static/logo/Rust.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/logo/Swift.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /static/logo/TypeScript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/logo/Vue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /static/logo/all.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 24 | 28 | 32 | 33 | 36 | 40 | 41 | 52 | 53 | 73 | 75 | 76 | 78 | image/svg+xml 79 | 81 | 82 | 83 | 84 | 85 | 89 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /static/logo/from.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 24 | 28 | 32 | 33 | 36 | 40 | 41 | 52 | 53 | 73 | 75 | 76 | 78 | image/svg+xml 79 | 81 | 82 | 83 | 84 | 85 | 89 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /static/main.js: -------------------------------------------------------------------------------- 1 | // ================================================================= 2 | //## show dropdown 3 | function showMenu(id_name = "drop-menu") { 4 | let show = document.getElementById(id_name); 5 | if (show) { show.classList.toggle("show");} 6 | } 7 | 8 | // get query param 9 | String.prototype.regexIndexOf = function(regex, startpos) { 10 | var indexOf = this.substring(startpos || 0).search(regex); 11 | return (indexOf >= 0) ? (indexOf + (startpos || 0)) : indexOf; 12 | } 13 | 14 | function getParam(param, query, startwith, delimit1, delimit2) { 15 | let start = 0, end = 0; 16 | // console.log(query); 17 | if (query.length > start && query.startsWith(startwith)) { 18 | start = query.search(param + delimit1); 19 | if (start != -1) { 20 | start = start + param.length + 1; 21 | end = delimit2 ? query.regexIndexOf(delimit2, start) : query.length; 22 | if (end === -1) { end = query.length; } 23 | } 24 | let c = query.substring(start, end); 25 | return c 26 | } 27 | return "" 28 | } 29 | 30 | function getQueryParam(param, query) { 31 | return getParam(param, query, startwith='?', delimit1='=', delimit2=/&[\w]+=/) 32 | } 33 | function getCookie(param) { 34 | return getParam(param, query=document.cookie, startwith='', delimit1='=', delimit2=';') 35 | } 36 | // tackle & in redirect query 37 | function getRedirect(param, query) { 38 | return getParam(param, query, startwith='?', delimit1='=', delimit2='') 39 | } 40 | 41 | // extract val or set val 42 | function getValsByIDs(ids=[], prefix='') { 43 | let vals = []; 44 | for ( let id of ids ) { 45 | let ele = document.getElementById(prefix + id); 46 | let val = ele ? ele.value : ''; 47 | vals.push(val); 48 | } 49 | return vals; 50 | } 51 | function setValsByIDs(ids=[], prefix='', vals={}) { 52 | for ( let id of ids ) { 53 | let ele = document.getElementById(prefix + id); 54 | let val = vals[id] 55 | if (ele.value === undefined) { 56 | ele.innerHTML = val || ''; 57 | } else { 58 | ele.value = val || ''; 59 | } 60 | } 61 | } 62 | 63 | const TOK = 'NoSeSNekoTr'; // for get cookie token 64 | const IDENT = 'YITnEdIr' // for get cookie identity 65 | //## action once window loaded 66 | window.addEventListener('DOMContentLoaded', function() { 67 | //# check if authed 68 | let iden = getCookie(IDENT); 69 | let loginLink = document.getElementById('login-link'); 70 | if (loginLink) { 71 | loginLink.setAttribute('href', iden ? `/@${iden}` : '/auth?to=signin'); 72 | loginLink.innerText = iden ? ':::' : 'Log In'; 73 | } 74 | }); 75 | // Close the dropdown menu if the user clicks outside of it 76 | window.onclick = function(event) { 77 | if (!event.target.matches('.toolbtn')) { 78 | let dropdowns = document.getElementsByClassName("dropdown-content"); 79 | let i; 80 | for (i = 0; i < dropdowns.length; i++) { 81 | let openDropdown = dropdowns[i]; 82 | if (openDropdown.classList.contains('show')) { 83 | openDropdown.classList.remove('show'); 84 | } 85 | } 86 | } 87 | } 88 | 89 | function onSearch(selector) { 90 | let q = document.getElementById(selector); 91 | if (q && q.value != "") { 92 | let openUrl = 'https://www.google.com/search?q=site:toplog.cc/%20'; 93 | window.open(openUrl + q.value, "_blank"); 94 | } 95 | } 96 | 97 | const PerPage = 42; 98 | let idxPage = 1; 99 | let hasMoreIdx = true; 100 | function loadMoreItems(topic='all', ty='Article') { 101 | if (!hasMoreIdx) { return; } 102 | idxPage += 1; 103 | fetch( 104 | `/moreitems/${topic}/${ty}?page=${idxPage}&perpage=${PerPage}` 105 | ).then(resp => { 106 | //console.log(resp); 107 | resp.text().then( r => { 108 | //console.log(r); 109 | if ( !Boolean(r) ) { 110 | console.log("No More"); 111 | hasMoreIdx = false; 112 | } 113 | window.scrollTo(0, document.body.scrollHeight); 114 | document.getElementById('item-list').innerHTML += r; 115 | }) 116 | }); 117 | } 118 | 119 | function toggleTop(id) { 120 | let omg = getCookie("oMg"); 121 | if (omg !== 'true') return; 122 | let tok = getCookie(TOK); 123 | fetch(`/api/items/${id}`, { 124 | method: 'PATCH', 125 | headers: { 'Authorization': tok }, 126 | }).then(_res => { 127 | let toggleEle = document.getElementById("t-" + id); 128 | if (toggleEle) { toggleEle.hidden = true } 129 | //console.log(res.data) 130 | }); 131 | } 132 | 133 | function upVote(id) { 134 | let tok = getCookie(TOK); 135 | let check = Boolean(tok); 136 | if (!check) { 137 | window.location.href = "/auth?to=signin"; 138 | return; 139 | } 140 | fetch(`/api/items/${id}?action=vote`, { 141 | method: 'PUT', 142 | headers: { 'Authorization': tok }, 143 | }).then(res => { 144 | res.json().then(r => { 145 | //console.log(r); 146 | let voteEle = document.getElementById("vote-" + id); 147 | if (voteEle) { 148 | //let voteNum = Number(voteEle.innerText); 149 | voteEle.innerText = r; 150 | let upEle = document.getElementById("up-" + id); 151 | if (upEle) { upEle.hidden = true } 152 | } 153 | }) 154 | }); 155 | } 156 | 157 | // md parse in backend 158 | function showFull(id) { 159 | let mdSelector = 'md-' + id; 160 | let btnSelector = 'btn-' + id; 161 | let btn = document.getElementById(btnSelector); 162 | let full = document.getElementById(mdSelector); 163 | let ifShowMore = btn.innerText == 'more...' ? true : false 164 | full.className = ifShowMore ? 'meta-sum' : 'hide-part meta-sum'; 165 | btn.innerText = ifShowMore ? 'less...' : 'more...'; 166 | } 167 | 168 | function openLink(link, admin=false) { 169 | let check = admin 170 | ? getCookie("oMg") === "true" 171 | : Boolean(getCookie(TOK)); 172 | if (!check && !admin) { 173 | let redi = document.location.origin + link; 174 | window.location.href = `/auth?to=signin&redirect=${redi}`; 175 | return; 176 | } 177 | if (!check) return; 178 | window.location.href = link; 179 | } 180 | 181 | function navTopic(topic, ty) { 182 | setCookie('topic', topic); 183 | let link = `/collection?tpc=${topic}&ty=${ty}`; 184 | window.location.href = link; 185 | } 186 | 187 | 188 | // auth 189 | // set cookie 190 | // credit to js-cookie 191 | function setCookie (key, value, attributes) { 192 | if (typeof document === 'undefined') return; 193 | 194 | attributes = Object.assign({secure: true, sameSite: 'Lax'}, attributes); 195 | 196 | if (typeof attributes.expires === 'number') { 197 | attributes.expires = new Date(Date.now() + attributes.expires * 864e5); 198 | } 199 | if (attributes.expires) { 200 | attributes.expires = attributes.expires.toUTCString(); 201 | } 202 | 203 | key = encodeURIComponent(key) 204 | .replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent) 205 | .replace(/[()]/g, escape); 206 | value = encodeURIComponent(value) 207 | .replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g, decodeURIComponent); 208 | 209 | let stringifiedAttributes = ''; 210 | for (let attributeName in attributes) { 211 | if (!attributes[attributeName]) { 212 | continue 213 | } 214 | stringifiedAttributes += '; ' + attributeName; 215 | if (attributes[attributeName] === true) { 216 | continue 217 | } 218 | // Considers RFC 6265 section 5.2: 219 | // ... 220 | // 3. If the remaining unparsed-attributes contains a %x3B (";") 221 | // character: 222 | // Consume the characters of the unparsed-attributes up to, 223 | // not including, the first %x3B (";") character. 224 | // ... 225 | stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]; 226 | } 227 | 228 | return (document.cookie = key + '=' + value + stringifiedAttributes) 229 | } 230 | 231 | function delCookie(key, attributes={}) { 232 | setCookie(key, '', Object.assign({expires: -1}, attributes)) 233 | } 234 | 235 | function signOut(to='/') { 236 | delCookie(TOK); 237 | delCookie(IDENT); 238 | // delCookie(CAN); 239 | delCookie('oMg'); 240 | window.location.href = to; 241 | } 242 | 243 | // gen today date 244 | // 245 | function getToday() { 246 | let now = new Date(); 247 | let dd = String(now.getDate()).padStart(2, '0'); 248 | let mm = String(now.getMonth() + 1).padStart(2, '0'); //January is 0! 249 | let yyyy = String(now.getFullYear()); 250 | let today = yyyy + '-' + mm + '-' + dd; 251 | return today; 252 | } 253 | 254 | // autosize textarea 255 | // 256 | const newEvtListener = (parent, type, listener) => parent.addEventListener(type, listener); 257 | function initAutoSize(ids=[]) { 258 | const autoSize = (id) => { 259 | let text = document.getElementById(id); 260 | const resize = () => { 261 | text.style.height = 'auto'; 262 | text.style.height = text.scrollHeight + 'px'; 263 | }; 264 | const delayedResize = () => { 265 | window.setTimeout(resize, 0); 266 | }; 267 | newEvtListener(text, 'change', resize); 268 | newEvtListener(text, 'focus', resize); 269 | newEvtListener(text, 'cut', delayedResize); 270 | newEvtListener(text, 'paste', delayedResize); 271 | newEvtListener(text, 'drop', delayedResize); 272 | newEvtListener(text, 'keydown', delayedResize); 273 | 274 | text.focus(); 275 | text.select(); 276 | resize(); 277 | }; 278 | 279 | for (let id of ids) { 280 | autoSize(id); 281 | } 282 | } 283 | 284 | // gen avatar 285 | // 286 | const bgColors = [ 287 | '#F48FB1', '#FF4081', '#9C27B0', '#673AB7', '#3F51B5', 288 | '#2196F3', '#03A9F4', '#795548', '#9E9E9E', '#53B6CB', 289 | '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', 290 | '#FFC107', '#FF9800', '#B3E5FC', '#607D8B', '#D793E7', 291 | ]; 292 | 293 | function randomColor(seed, colors) { 294 | return colors[seed % (colors.length)] 295 | } 296 | 297 | function genStyle(src, size, uname, inline, isImage, rounded) { 298 | const style = { 299 | width: `${size}px`, 300 | height: `${size}px`, 301 | 'border-radius': rounded ? '50%' : 0, 302 | }; 303 | const backgroundAndFontStyle = isImage 304 | ? { 305 | background: `transparent url('${src}') no-repeat scroll 0% 0% / ${size}px ${size}px content-box border-box`, 306 | 'referrer-policy': 'no-referrer' 307 | } 308 | : { 309 | 'background-color': randomColor(uname.length, bgColors), 310 | font: Math.floor(size / 2) + 'px/100px Helvetica, Arial, sans-serif', 311 | 'font-weight': 'bold', 312 | 'line-height': `${(size + Math.floor(size / 20))}px`, 313 | display: inline ? 'inline-flex' : 'flex', 314 | 'align-items': 'center', 315 | 'justify-content': 'center', 316 | // color: randomColor(uname.length + 1, bgColors) 317 | }; 318 | Object.assign(style, backgroundAndFontStyle); 319 | let stringStyle = ''; 320 | for (let attr in style) { 321 | if (!style[attr]) { 322 | continue 323 | } 324 | stringStyle += attr + ': ' + style[attr] + '; '; 325 | } 326 | return stringStyle 327 | } 328 | 329 | function initial(uname) { 330 | let parts = uname.split(/[ _-]/); 331 | let initials = ''; 332 | for (let i = 0; i < parts.length; i++) { 333 | initials += parts[i].charAt(0); 334 | } 335 | if (initials.length > 3 && initials.search(/[A-Z]/) !== -1) { 336 | initials = initials.replace(/[a-z]+/g, ''); 337 | } 338 | initials = initials.substr(0, 3).toUpperCase(); 339 | return initials 340 | } 341 | 342 | function initAvatar(ctnID, src, size, uname, rounded=false, inline=false) { 343 | let avatar = document.createElement('div'); 344 | let isImage = Boolean(src); 345 | let GenStyle = genStyle(src, size, uname, inline, isImage, rounded); 346 | avatar.setAttribute('style', GenStyle); 347 | 348 | if (!isImage) { 349 | let inner = document.createElement('span'); 350 | inner.innerText = initial(uname); 351 | avatar.appendChild(inner); 352 | } 353 | let avatarCtn = document.getElementById(ctnID); 354 | if (avatarCtn) { avatarCtn.appendChild(avatar); } 355 | } 356 | -------------------------------------------------------------------------------- /static/profile.js: -------------------------------------------------------------------------------- 1 | let uname; 2 | let extKw; 3 | let page = 1; 4 | let totalCount; 5 | let hasMore = false; 6 | 7 | document.addEventListener('DOMContentLoaded', async function() { 8 | let srcSpan = document.getElementById('avatar-src'); 9 | let src = srcSpan ? srcSpan.innerText : ''; 10 | let name = document.getElementById('avatar-name'); 11 | uname = name ? name.innerText : ''; 12 | initAvatar('user-avatar', src, 180, uname); 13 | await navTo('vote', 'rl'); 14 | }) 15 | 16 | // submit/vote 17 | async function navTo(kw, id) { 18 | extKw = kw; 19 | await loadAndAppend(kw); 20 | // active tab 21 | let active = document.getElementById(id); 22 | let tabs = document.getElementsByClassName('s-nav'); 23 | for (let tab of tabs ) { tab.classList.remove("active-tab"); } 24 | active.classList.add("active-tab"); 25 | } 26 | 27 | async function loadMoreAny() { 28 | if (!hasMore) return; 29 | page += 1; 30 | await loadAndAppend(extKw, true); 31 | } 32 | 33 | // load list and generate html then append to page 34 | async function loadAndAppend(action, isMore=false) { 35 | let url = `/api/getitems/user?per=${uname}&kw=${action}&page=${page}&perpage=${PerPage}`; 36 | let resp = await fetch(url); 37 | if (!resp.ok) return; 38 | let res = await resp.json(); 39 | let objs = res.items; 40 | if (page == 1) { totalCount = res.count; } 41 | hasMore = page <= Math.floor(totalCount / PerPage); 42 | 43 | let moreBtn = document.getElementById('loadmore-btn'); 44 | if (moreBtn) { 45 | if (hasMore) { 46 | moreBtn.removeAttribute('disabled'); 47 | } else { 48 | moreBtn.setAttribute('disabled', true); 49 | } 50 | moreBtn.style.display = hasMore ? '' : 'none'; 51 | } 52 | 53 | let sList = document.getElementById('nav-list-box'); 54 | if (!isMore) { sList.innerHTML = ''; } 55 | 56 | for (let obj of objs) { 57 | let sum = document.createElement('section'); 58 | sum.className = 's-sum-info'; 59 | let title = obj.title; 60 | let intro = obj.content; 61 | let link = obj.link || ('/item/' + obj.id); 62 | let inner = ` 63 | 64 | ${title} 65 | 66 |
${intro}
67 | `; 68 | sum.innerHTML = inner; 69 | sList.appendChild(sum); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Lato,Roboto,Open Sans,sans-serif; 3 | font-size: 18px; 4 | line-height: 1.8em; 5 | background-color: #f7f5f3; 6 | overflow-wrap: break-word; 7 | } 8 | a { 9 | color: rgba(0,0,0,.75); 10 | text-decoration: none; 11 | } 12 | a:hover { 13 | color: #03a87c; 14 | } 15 | h2, h3, h4 { 16 | margin: 8px 0; 17 | line-height: 1.2em; 18 | font-family: Pontano Sans, Lato, Roboto, Open Sans, serif; 19 | } 20 | p { 21 | margin: 5px 0; 22 | } 23 | pre { 24 | background-color: #f0f0f0; 25 | padding: 5px; 26 | } 27 | blockquote { 28 | border-left: 3px solid #03a87c; 29 | margin: 5px; 30 | padding-left: 12px; 31 | } 32 | textarea { 33 | resize: vertical; 34 | } 35 | 36 | .full-width { 37 | width: 100%; 38 | } 39 | .help-tips { 40 | color: green; 41 | font-size: 12px; 42 | } 43 | .header { 44 | z-index: 999; 45 | height: 45px; 46 | /* min-width: 240px; */ 47 | background-color: #f1f3f2; 48 | /* box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2); */ 49 | } 50 | 51 | .site-logo { 52 | /* width: 75px; */ 53 | padding: 0 5px; 54 | } 55 | .nav-menu { 56 | box-sizing: border-box; 57 | margin: 0px auto; 58 | padding: 6px 0px; 59 | display: flex; 60 | } 61 | .logo { 62 | color: #03a87c; 63 | font-size: 1.2em; 64 | letter-spacing: 0.005em; 65 | } 66 | .logo-sup, .logo-sub { 67 | font-size: 0.5em; 68 | color: grey; 69 | } 70 | .logo-sup { 71 | margin-left: -1em; 72 | } 73 | .main-menu, .input-nav { 74 | flex: 1; 75 | padding: 5px 0px 0px 0px; 76 | } 77 | .search-box { 78 | /* display: inline-block; */ 79 | margin-left: 10px; 80 | } 81 | .search-box .search-text { 82 | height: 18px; 83 | width: 240px; 84 | } 85 | .nav-menu .right-menu { 86 | float: right; 87 | } 88 | .toolbtn { 89 | color: #3498db; 90 | padding: 5px 15px; 91 | font-size: 12px; 92 | border: none; 93 | cursor: pointer; 94 | } 95 | .toolbtn:hover, .toolbtn:focus { 96 | background-color: #2980b9; 97 | color: white; 98 | } 99 | .menudown { 100 | position: relative; 101 | display: inline-block; 102 | } 103 | .dropdown-content { 104 | display: none; 105 | position: absolute; 106 | background-color: #f1f1f1; 107 | font-size: 14px; 108 | min-width: 100px; 109 | overflow: auto; 110 | box-shadow: 0px 8px 8px 0px rgba(0,0,0,0.2); 111 | z-index: 999; 112 | } 113 | .dropdown-content a { 114 | padding: 2px 5px; 115 | text-decoration: none; 116 | } 117 | .menudown li { 118 | list-style-type:none; 119 | } 120 | .menudown li:hover { 121 | background-color: #cae3f3; 122 | } 123 | .show { 124 | display: block; 125 | } 126 | 127 | .footer { 128 | margin-top: 3em; 129 | background-color: #f5f5f5; 130 | /* min-width: 240px; */ 131 | box-shadow: 0 -2px 2px -1px rgba(0,0,0,.2); 132 | } 133 | .footer .bottom { 134 | margin: 5px auto; 135 | font-size: 0.75em; 136 | } 137 | .sub-link{ 138 | color: #03a87c; 139 | } 140 | 141 | .banner { 142 | background-color: #f6f9f8; 143 | padding: 5px 5px 0px 5px; 144 | border-bottom: 0.2px dashed #63a191; 145 | } 146 | .banner-bar { 147 | box-sizing: border-box; 148 | margin: 0px auto; 149 | padding: 0px; 150 | position: relative; 151 | } 152 | 153 | .topic-tab, .ty-tab, 154 | .topic-tab-active, .ty-tab-active { 155 | margin-right: 10px; 156 | } 157 | .topic-tab{ 158 | font-size: 16px; 159 | } 160 | .ty-tab{ 161 | font-size: 14px; 162 | } 163 | .topic-tab-active { 164 | background-color: #2980b9; 165 | padding: 5px 10px; 166 | font-size: 16px; 167 | color: #fff; 168 | font-weight: 600; 169 | border-radius: 3px; 170 | } 171 | .ty-tab-active { 172 | border-bottom: 4px solid #03a87c; 173 | padding: 2px 2px 6px 2px; 174 | } 175 | 176 | .main-view { 177 | min-height: 42em; 178 | width: 100%; 179 | margin: 0 auto; 180 | position: relative; 181 | } 182 | 183 | .header .nav-menu, 184 | .footer .bottom, 185 | .banner .banner-bar, 186 | .main-view { 187 | max-width: 960px; 188 | /* min-width: 240px; */ 189 | } 190 | 191 | .main-page, .info-page { 192 | display: flex; 193 | flex-direction: row; 194 | } 195 | .page-main { 196 | flex: 7; 197 | margin: 10px 0 0 0; 198 | } 199 | .page-side { 200 | flex: 3; 201 | /* background-color: #fafbfa; */ 202 | margin: 10px 0 30px 10px; 203 | } 204 | .note { 205 | padding: auto; 206 | } 207 | .note-content { 208 | font-size: 15px; 209 | color: #828282; 210 | padding: 2px; 211 | } 212 | 213 | .right-title { 214 | background-color: #e5ebe4; 215 | padding: 10px; 216 | font-size: 16px; 217 | border-left: 5px solid #03a87c; 218 | } 219 | .right-body { 220 | padding: 10px; 221 | font-size: 16px; 222 | } 223 | 224 | .link-icon { 225 | margin-right: 4px; 226 | } 227 | .link-icon:hover { 228 | border-top: 2px solid #03a87c; 229 | } 230 | .hide-link { 231 | color: transparent; 232 | } 233 | .hide-link:hover { 234 | color: #03a87c; 235 | } 236 | 237 | .item-sum, .item-view { 238 | background-color:#f5f7f8; 239 | padding: 10px; 240 | min-height: 75px; 241 | } 242 | .item-sum { 243 | border-bottom: 1px dashed #ddd; 244 | } 245 | .item-sum:hover { 246 | background-color: #fcfdfc; 247 | } 248 | .item-info { 249 | display: flex; 250 | flex-direction: row; 251 | } 252 | .item-box { 253 | flex: 9; 254 | padding: 2px; 255 | } 256 | .thumb-box { 257 | flex: 1; 258 | padding: 2px; 259 | } 260 | .item-title { 261 | font-size: 22px; 262 | } 263 | .item-meta { 264 | font-size: 14px; 265 | } 266 | 267 | .meta-tag { 268 | margin-right: 8px; 269 | } 270 | .meta-sum { 271 | color: #828282; 272 | font-size: 16px; 273 | max-height: 160px; 274 | overflow: hidden; 275 | } 276 | .meta-sum:hover { 277 | overflow: auto; 278 | } 279 | 280 | .item-view .title { 281 | font-size: 24px; 282 | } 283 | .item-md { 284 | color: #98a3ad; 285 | font-size: 18px; 286 | } 287 | .item-img { 288 | border-radius: 2px; 289 | max-width: 100%; 290 | max-height: 42%; 291 | } 292 | .item-thumb { 293 | border-radius: 2.5%; 294 | max-width: 160px; 295 | max-height: 160px; 296 | } 297 | .vote-num { 298 | font-size: 0.75em; 299 | color: #fff; 300 | background-color: #aab0c6; 301 | padding: 0px 2px; 302 | border-radius: 25%; 303 | } 304 | .more-opt-btn { 305 | padding: 1px 2px; 306 | } 307 | .active-tab { 308 | border-bottom: 2px solid #03a87c; 309 | color: #03a87c; 310 | } 311 | 312 | .profile-head { 313 | display: flex; 314 | flex-direction: row; 315 | } 316 | .profile-info, .profile-avatar { 317 | padding: 5px 10px; 318 | } 319 | .profile-avatar { 320 | width: 200px; 321 | } 322 | .profile-info { 323 | flex: 1; 324 | } 325 | .profile-name { 326 | font-size: 1.5em; 327 | } 328 | .profile-aboutme { 329 | font-size: 0.85em; 330 | padding: 5px 0px; 331 | } 332 | .profile-intro { 333 | max-height: 120px; 334 | overflow: hidden; 335 | } 336 | .profile-intro:hover { 337 | overflow: auto; 338 | } 339 | .profile-nav { 340 | margin-top: 10px; 341 | border-bottom: 1px dashed #03a87c; 342 | border-top: 1px dashed #03a87c; 343 | } 344 | .s-nav { 345 | padding: 7px 4px; 346 | font-size: 0.85em; 347 | color: rgba(0,0,0,.75); 348 | } 349 | .s-list { 350 | background-color: #f5f7f8; 351 | } 352 | .s-sum-info { 353 | padding: 10px; 354 | border-bottom: 1px dashed #ddd; 355 | } 356 | .s-sum-info:hover { 357 | background-color: #e7e9e9; 358 | } 359 | .s-sum { 360 | color: #828282; 361 | font-size: 16px; 362 | max-height: 160px; 363 | overflow: hidden; 364 | } 365 | .s-sum:hover { 366 | overflow: auto; 367 | } 368 | 369 | .hide-submit-box { 370 | display: none; 371 | background-color: #f0efee; 372 | } 373 | .form-wrap, .form-item-wrap { 374 | padding: 10px 5px; 375 | display: flex; 376 | flex-direction: row; 377 | } 378 | .as-select, .input-sum-date { 379 | width: 95px; 380 | background-color: #f5f5f5; 381 | border: 2px solid #fff; 382 | } 383 | .input-sum-date { 384 | width: 130px; 385 | } 386 | .input-box { 387 | flex: 1; 388 | display: block; 389 | padding: 10px; 390 | background-color: #ecebea; 391 | border: 2px solid #fff; 392 | } 393 | .submit-btn { 394 | width: 95px; 395 | } 396 | 397 | .input-nav { 398 | overflow: hidden; 399 | } 400 | .form-page { 401 | padding: 5px; 402 | border: 1px solid #f3f3f3; 403 | } 404 | .form-box { 405 | padding: 0 10px 0 0; 406 | border-right: 3px solid #e1ebec; 407 | } 408 | .form-item-label { 409 | width: 10px; 410 | } 411 | .form-tips { 412 | padding: 2px 10px; 413 | color: green; 414 | } 415 | 416 | .form-input { 417 | padding: 5px 10px; 418 | box-sizing: border-box; 419 | background: #ecebea; 420 | width: 100%; 421 | line-height: 1.42; 422 | border: 0; 423 | } 424 | .form-input-title { 425 | font-size: 1.05em; 426 | line-height: 1.5em; 427 | font-weight: 500; 428 | height: 32px; 429 | } 430 | .form-input-intro { 431 | font-size: 0.9em; 432 | line-height: 1.5em; 433 | } 434 | .form-input-link { 435 | font-size: 0.85em; 436 | height: 32px; 437 | } 438 | .form-input-select { 439 | font-size: 0.8em; 440 | } 441 | .cache-tips, .autosave-tips, .form-help-tips { 442 | background-color: #f8f6e3; 443 | font-size: 14px; 444 | padding: 5px; 445 | margin: 5px 0; 446 | } 447 | .viaurl-form { 448 | width: 100%; 449 | background-color: #f0efee; 450 | margin: 5px 0; 451 | } 452 | .tags-container { 453 | padding: 2px 10px; 454 | } 455 | .new-form-tag { 456 | padding: 5px; 457 | margin-right: 3px; 458 | background: #ddd; 459 | font-size: 12px; 460 | } 461 | .new-form-tag:hover { 462 | background: #8fc4d4; 463 | } 464 | .edit-tag-btn { 465 | font-size: 12px; 466 | color: red; 467 | cursor: pointer; 468 | } 469 | 470 | .form-submit-btn { 471 | width: 100%; 472 | padding: 10px 15px; 473 | font-size: 16px; 474 | background-color:#dce6e2; 475 | } 476 | 477 | .bookmarklet-tips { 478 | font-size: 20px; 479 | text-align: center; 480 | background-color: #e6ebf2; 481 | padding: 10px 20px; 482 | } 483 | 484 | .badge, .badge-circle, .badge-heart, 485 | .badge-bead, .badge-oval { 486 | flex: 0 0 auto; 487 | margin-right: 1px; 488 | display: inline-block; 489 | background-color: #03a87c; /*default*/ 490 | } 491 | .badge { 492 | width: 8px; 493 | height: 10px; 494 | } 495 | .badge-circle { 496 | width: 10px; 497 | height: 10px; 498 | border-radius: 50%; 499 | } 500 | .badge-bead { 501 | width: 8px; 502 | height: 10px; 503 | border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; 504 | } 505 | .badge-oval { 506 | width: 6px; 507 | height: 10px; 508 | border-radius: 50% / 50%; 509 | background-color: #03a83a; 510 | } 511 | .badge-heart { 512 | position: relative; 513 | width: 10px; 514 | height: 9px; 515 | background-color: transparent; 516 | } 517 | .badge-heart:before, 518 | .badge-heart:after { 519 | position: absolute; 520 | content: ""; 521 | left: 5px; 522 | top: 0; 523 | width: 5px; 524 | height: 8px; 525 | background: rgb(231, 140, 140); 526 | border-radius: 5px 5px 0 0; 527 | transform: rotate(-45deg); 528 | transform-origin: 0 100%; 529 | } 530 | .badge-heart:after { 531 | left: 0; 532 | transform: rotate(45deg); 533 | transform-origin: 100% 100%; 534 | } 535 | .triangle-up { 536 | flex: 0 0 auto; 537 | width: 0; 538 | height: 0; 539 | border-left: 5px solid transparent; 540 | border-right: 5px solid transparent; 541 | border-bottom: 10px solid #999999; 542 | margin-right: 1px; 543 | display: inline-block; 544 | text-align: justify; 545 | } 546 | 547 | .Article { 548 | background-color: #3ab54a; 549 | } 550 | .Book { 551 | background-color: #5a4129; 552 | } 553 | .Event { 554 | background-color: #f7d514; 555 | } 556 | .Job { 557 | background-color: #18f3d6; 558 | } 559 | .Media { 560 | background-color: #c372f1; 561 | } 562 | .Project { 563 | background-color: #da552f; 564 | } 565 | .Translate { 566 | background-color: #2e62f3; 567 | } 568 | 569 | .tab-Rust { 570 | background-color: #dea584; 571 | } 572 | .tab-Go { 573 | background-color: #00add8; 574 | } 575 | .tab-Swift { 576 | background-color: #ffac45; 577 | } 578 | .tab-CPP { 579 | background-color: #f34b7d; 580 | } 581 | .tab-C { 582 | background-color: #555555; 583 | } 584 | .tab-C-sharp { 585 | background-color: #178600; 586 | } 587 | .tab-Python { 588 | background-color: #3572a5; 589 | } 590 | .tab-TypeScript { 591 | background-color: #2b7489; 592 | } 593 | .tab-JavaScript { 594 | background-color: #f1e05a; 595 | } 596 | .tab-Dart { 597 | background-color: #00b4ab; 598 | } 599 | .tab-Java { 600 | background-color: #b07219; 601 | } 602 | .tab-Kotlin { 603 | background-color: #f18133; 604 | } 605 | .tab-PHP { 606 | background-color: #4f5d95; 607 | } 608 | .tab-Vue { 609 | background-color: #34495e; 610 | } 611 | .tab-Angular { 612 | background-color: #c3002f; 613 | } 614 | .tab-React { 615 | background-color: #61dafb; 616 | } 617 | .tab-Web { 618 | background-color: #60c1e4; 619 | } 620 | .pad { 621 | padding: 5px; 622 | } 623 | 624 | .blog-info { 625 | padding-right: 45px; 626 | min-height: 55px; 627 | position: relative; 628 | } 629 | .blog-intro { 630 | color: #828282; 631 | font-size: 14px; 632 | line-height: 1.5em; 633 | } 634 | .blog-meta { 635 | font-size: 12px; 636 | } 637 | .blog-thumb { 638 | position: absolute; 639 | max-width: 42px; 640 | max-height: 52px; 641 | top: 8px; 642 | right: 0px; 643 | border-radius: 50%; 644 | } 645 | 646 | .info-box { 647 | flex: 1; 648 | max-width: 750px; 649 | } 650 | 651 | @media (max-width: 800px) { 652 | .main-page, .profile-head { 653 | padding: 10px; 654 | flex-direction: column; 655 | } 656 | } 657 | 658 | @media (max-width: 480px) { 659 | .item-info { 660 | flex-direction: column; 661 | } 662 | } 663 | 664 | @media (prefers-color-scheme: dark) { 665 | body, .header, .banner, .footer { 666 | background-color: #282a36; 667 | color: #e4e4e4; 668 | } 669 | .item-sum:hover { 670 | background-color: #373a47; 671 | color: #e4e4e4; 672 | } 673 | .right-title, .dropdown-content, .item-sum, .item-view 674 | { 675 | background-color: #353746; 676 | color: #e4e4e4; 677 | } 678 | .header, .item-sum { 679 | border-bottom: 2px solid #282827; 680 | } 681 | .toolbtn { 682 | background-color: #2e2f3b; 683 | } 684 | pre { 685 | background-color: rgb(56, 55, 55); 686 | } 687 | a { 688 | color: #e4e4e4; 689 | } 690 | img { 691 | filter: grayscale(30%); 692 | } 693 | } 694 | -------------------------------------------------------------------------------- /task.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd /path_to/job 3 | ./job 4 | 5 | ## chmod +x task.sh 6 | ## crontab -e: 0 9 * * * /path_to/task.sh >> /path_to/srv.log 2>&1 7 | ## nohup ./web & 8 | ## nohup ./worker & 9 | -------------------------------------------------------------------------------- /templates/0_blog_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%- block title -%} Suggest A Blog {%- endblock title -%} 4 | 5 | {%- block navMain -%} 6 |
7 | Suggest A Blog 8 |
9 | {%- endblock navMain -%} 10 | 11 | {%- block mainview -%} 12 |
13 |
14 |
15 | Blogger Name* 16 |
17 | 18 |
19 | Blog Name* 20 |
21 | 22 |
23 | Blog Link* 24 |
25 | 26 |
27 | Select Domain* 28 |
29 | 35 |
36 | Blog Logo* 37 |
38 | 39 |
40 | Introduction* 41 |
42 | 43 |
44 |
45 | 69 |
70 | {%- endblock mainview -%} 71 | 72 | {%- block script -%} 73 | 74 | 75 | {%- endblock script -%} 76 | -------------------------------------------------------------------------------- /templates/0_item_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%- block title -%} 4 | {%- if is_new -%} Suggest New Item {%- else -%} Edit Item {%- endif -%} 5 | {%- endblock title -%} 6 | 7 | {%- block navMain -%} 8 |
9 | 10 | {%- if is_new -%} Suggest New Item {%- else -%} Edit Item {%- endif -%} 11 | 12 |
13 | {%- endblock navMain -%} 14 | 15 | {%- block mainview -%} 16 |
17 | {%- if is_new -%} 18 |
19 | > Submit Link to Suggest New Item 20 |
21 |
22 |
23 |
24 | 25 | 28 |
29 |
30 |
31 |
32 | > Fill the Form to Suggest New Item 33 |
34 | {%- endif -%} 35 | 36 |
37 |
38 | Title* 39 |
40 | 41 |
42 | Summary* 43 |
44 | 45 |
46 | Link* 47 |
48 | 49 |
50 | Author* 51 |
52 | 53 |
54 | Select Category and Domain* 55 |
56 | 62 | 68 |
69 |
70 | 71 | 93 |
94 | {%- endblock mainview -%} 95 | 96 | {%- block script -%} 97 | 98 | {%- endblock script -%} 99 | -------------------------------------------------------------------------------- /templates/0_submit_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%- block title -%} Submit An Item {%- endblock title -%} 4 | 5 | {%- block navMain -%} 6 |
7 | Submit An Item 8 |
9 | {%- endblock navMain -%} 10 | 11 | {%- block mainview -%} 12 |
13 |
14 |
15 | 16 |
17 | 18 |
19 | Link* 20 |
21 | 22 |
23 | Author* 24 |
25 | 26 |
27 | Summary* 28 |
29 | 30 |
31 | Select Category and Domain* 32 |
33 | 39 | 45 |
46 |
47 | 48 | 64 |
65 | {%- endblock mainview -%} 66 | 67 | {%- block script -%} 68 | 69 | {%- endblock script -%} 70 | -------------------------------------------------------------------------------- /templates/_auth_box.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 | 9 | Notice: Use Cookies Here to Assist Authentication. 10 | 11 |
12 | 15 |
16 | 17 | If forget Password or New to here, Please. 18 | 19 |
20 | 39 | 53 | 69 | 97 | -------------------------------------------------------------------------------- /templates/_banner.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | -------------------------------------------------------------------------------- /templates/_blog_sum.html: -------------------------------------------------------------------------------- 1 |
2 | {%- let cname = c.clone().aname -%} 3 | {%- let c_name = cname|b64_encode -%} 4 | {%- let c_topic = c.clone().topic -%} 5 | {%- let blog_link = c.clone().blog_link -%} 6 | {%- let gh_link = c.clone().gh_link -%} 7 | {%- let ot_link = c.clone().other_link -%} 8 | {%- let c_intro = c.clone().intro -%} 9 | {%- let blog_avatar = c.clone().avatar -%} 10 |
11 | 12 | {{ cname }} 13 | 14 |
15 | 16 | {{ c_topic }} 17 | .. 18 |
19 | {%- if blog_avatar.len() > 0 -%} 20 | 21 | 22 | 23 | {%- endif -%} 24 |
25 | 43 |
44 | -------------------------------------------------------------------------------- /templates/_bookmarklet.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | Post to Toplog Bookmarklet 6 | 7 | -------------------------------------------------------------------------------- /templates/_item_sum.html: -------------------------------------------------------------------------------- 1 |
2 | {%- let a_url = item.link.clone() -%} 3 | {%- let a_content = item.content.clone() -%} 4 | {%- let a_author = item.author.clone()|b64_encode -%} 5 | 6 | {%- let itmid = item.id -%} 7 | {%- let a_topic = item.topic.clone() -%} 8 | {%- let a_ty = item.ty.clone() -%} 9 |
10 | 18 | {%- let logo = item.logo.clone() -%} 19 | {%- if logo.len() > 0 -%} 20 |
21 | 22 |
23 | {%- endif -%} 24 |
25 |
26 | 27 | {{ a_ty }} 28 | 29 | {%- if a_topic != topic -%} 30 | 31 | 32 | {{ a_topic }} 33 | 34 | 35 | {%- else -%} 36 | {{ a_topic }} 37 | {%- endif -%} 38 | 39 | {{ item.author }} 40 | 41 | 42 | {{ item.vote }} 43 | 44 | 45 | 46 | 58 |
59 |
60 | -------------------------------------------------------------------------------- /templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%- block title -%} About {%- endblock title -%} 4 | 5 | {%- block banner -%} {%- endblock banner -%} 6 | 7 | {%- block mainview -%} 8 |
9 |
10 |
11 |

About

12 | An Aggregator for Programmers. 13 |
14 |
15 |

Privacy Policy

16 | We won't collect or store or share any private information. 17 |
18 |
19 |

Terms of Service

20 | 21 | - Your access to and use of the Service is conditioned upon your acceptance 22 | of and compliance with the Terms of Service. The terms of Service may change over time.
23 | - We are constantly adding new features/content and do our best to keep the service operational, but we cannot offer any guarantees.
24 | - You own and are responsible for the content that you post.
25 | - We do not control the content that user post and do not provide any assurances regarding its validity, reliability or accuracy.
26 | - We will respond to any claim that Content posted on the Service infringes on the copyright or other intellectual property rights of any person or entity.
27 |
28 | REMINDER:
29 | We use Google Analytics tools, You can opt-out it, please visiting Opt-out Google Analytics 30 |
31 | 35 |
36 |
37 | {%- endblock mainview -%} 38 | -------------------------------------------------------------------------------- /templates/auth_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%- block title -%} Welcome {%- endblock title -%} 4 | 5 | {%- block navMain -%}{%- endblock navMain -%} 6 | 7 | {%- block mainview -%} 8 |
9 |
10 |

Welcome

11 | {%- include "_auth_box.html" -%} 12 |
13 |
14 | {%- endblock mainview -%} 15 | 16 | {%- block script -%} 17 | 18 | 19 | {%- endblock script -%} 20 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {# coupled with js / css, donot change!! #} 4 | {%- let iTopics = vec!("C", "Dart", "DataBase", "Go", "Java", "JavaScript", "Kotlin", "Python", "PHP", "Rust", "Swift", "TypeScript") -%} 5 | {%- let iCates = vec!("Article", "Book", "Event", "Job", "Media", "Project", "Translate", "Misc") -%} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {%- block head -%} 14 | Toplog | {%- block title -%} Home {%- endblock title -%} 15 | {%- block metaDescription -%} 16 | 17 | {%- endblock metaDescription -%} 18 | {%- endblock head -%} 19 | 20 | 21 | 22 | {%- block body -%} 23 |
24 | {%- block header -%} 25 | 50 | {%- endblock header -%} 51 |
52 |
53 | {%- block mainview -%}{%- endblock mainview -%} 54 |
55 |
56 | {%- block foot -%} 57 |
58 | ©Toplog: Dispatch, Discuss, Distill | About 59 | | Issues 60 | | {%- include "_bookmarklet.html" -%} 61 | 62 | 65 | 66 |
67 | 68 | 69 | More Topic: 70 | 71 | 72 | {%- for tpc in iTopics -%} 73 | 74 | {{ tpc }} 75 | 76 | {%- endfor -%} 77 |
78 |
79 |
80 | {%- endblock foot -%} 81 |
82 | {%- endblock body -%} 83 | 84 | {%- block script -%}{%- endblock script -%} 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /templates/collection.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%- block title -%} {{ ty }} on {{ topic }} {%- endblock title -%} 4 | 5 | 6 | 7 | {%- block mainview -%} 8 | 11 |
12 |
13 |
14 | {%- if ty == "Article" -%} 15 | Highlights, Opinions and Stories 16 | {%- endif -%} 17 | {%- if ty == "Book" -%} 18 | Guided learning journey & Featured books 19 | {%- endif -%} 20 | {%- if ty == "Event" -%} 21 | Events, Talks and Playlist 22 | {%- endif -%} 23 | {%- if ty == "Job" -%} 24 | Who is Hiring? 25 | {%- endif -%} 26 | {%- if ty == "Media" -%} 27 | Podcast or Video or Cartoon... 28 | {%- endif -%} 29 | {%- if ty == "Project" -%} 30 | Show your Creatives, Code and Tools 31 | {%- endif -%} 32 | {%- if ty == "Translate" -%} 33 | Share your translation 34 | {%- endif -%} 35 | {%- if ty == "Misc" -%} 36 | 37 | The Newest that have not yet hoisted or Raw links via spider... 38 | 39 | {%- endif -%} 40 | {%- if topic == "from" -%} 41 | By {{ ty }} : 42 | {%- endif -%} 43 |
44 |
45 | {%- for item in items -%} 46 | {%- include "_item_sum.html" -%} 47 | {%- endfor -%} 48 |
49 | {%- if topic == "from" -%} 50 | 51 | {%- else -%} 52 | 53 | {%- endif -%} 54 |
55 |
56 |

Featured Contributors

57 |
58 | {%- for c in blogs -%} 59 | {%- include "_blog_sum.html" -%} 60 | {%- endfor -%} 61 |
62 |
63 | 64 | Install the Toplog Bookmarklet to submit page with a single click. 65 | To Install, Just Drag or Right Click the below Link to bookmark it: 66 |
67 | {%- include "_bookmarklet.html" -%} 68 |
69 |
70 |
71 | {%- endblock mainview -%} 72 | -------------------------------------------------------------------------------- /templates/item.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%- block title -%} {{ item.title }} {%- endblock title -%} 4 | {%- block banner -%} {%- endblock banner -%} 5 | 6 | {%- block mainview -%} 7 |
8 |
9 |
10 | {%- let a_url = item.clone().link -%} 11 | {%- let a_content = item.clone().content -%} 12 | {%- let a_author = item.clone().author|b64_encode -%} 13 | 14 | {%- let itmid = item.id -%} 15 | 16 | {{ item.title }} 17 | | 18 | {{ a_url|host }} 19 | 20 |
21 | 22 | {{ item.ty }} 23 | 24 | {{ item.topic }} 25 | 26 | {{ item.author }} 27 | {{ item.pub_at|date_fmt("%b %d, %Y") }} 28 | ...   31 | 34 | {%- if item.is_top -%} .. {%- else -%} ::: {%- endif -%} 35 | 36 |
37 | {%- let logo = item.logo.clone() -%} 38 | {%- if logo.len() > 0 -%} 39 | 40 | 41 | 42 | {%- endif -%} 43 |
{{ a_content|md|safe }}
44 |
45 |
46 |
47 |
48 | {%- endblock mainview -%} 49 | -------------------------------------------------------------------------------- /templates/more_item.html: -------------------------------------------------------------------------------- 1 | {%- for item in items -%} 2 | {%- include "_item_sum.html" -%} 3 | {%- endfor -%} -------------------------------------------------------------------------------- /templates/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title -%} {{ user.nickname }}@{{ user.uname }} {% endblock title -%} 4 | 5 | {% block mainview -%} 6 |
7 |
8 |
9 |
10 | 11 | 12 |
13 |
14 | {% if user.nickname.len() > 0 %} 15 | {{ user.nickname }} 16 | {% else %} 17 | {{ user.uname }} 18 | {% endif %} 19 | {% if is_self %} 20 | @{{ user.uname }} 21 | {% else %} 22 | @{{ user.uname }} 23 | {% endif %} 24 |
25 |
Joined {{ user.join_at|dt_fmt("%b %d, %Y") }}
26 |
{{ user.location }}
27 |
{{ user.intro }}
28 |
29 |
30 |
31 | 39 | 43 |
44 | 47 |
48 | {% endblock mainview -%} 49 | 50 | {%- block script -%} 51 | 52 | {%- endblock script -%} 53 | -------------------------------------------------------------------------------- /templates/sitemap/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://toplog.cc/ 5 | Daily 6 | 1.0 7 | 8 | {%- for t in tys -%} 9 | {%- for tp in topics -%} 10 | 11 | https://toplog.cc/collection?tpc={{ tp }}&ty={{ t }} 12 | Daily 13 | 0.9 14 | 15 | {%- endfor -%} 16 | {%- endfor -%} 17 | 18 | --------------------------------------------------------------------------------