├── deploy ├── .gitignore ├── build.sh └── docker-compose.yml ├── docs-website ├── docs │ ├── 開發 │ │ ├── 本地運行.md │ │ └── 環境設定.md │ ├── 基礎 │ │ ├── 設定檔一覽.md │ │ ├── 碳鍵是什麼.md │ │ ├── 安裝.md │ │ └── 電郵設定.md │ ├── 早期文件 │ │ ├── img │ │ │ ├── 鍵結關係.png │ │ │ ├── ptt討論串.png │ │ │ ├── 課程評價表單.png │ │ │ ├── ptt課程評價_1.png │ │ │ ├── ptt課程評價_2.png │ │ │ └── ptt課程評價模板.png │ │ ├── docker啓動.md │ │ ├── rust設置.md │ │ ├── 資料庫設置.md │ │ ├── 前端設置.md │ │ └── 快速開始.md │ ├── 進階 │ │ ├── 架構.md │ │ └── HTTPS.md │ ├── index.md │ ├── .vitepress │ │ └── config.ts │ └── public │ │ └── icon.svg ├── .gitignore ├── README.md └── package.json ├── frontend ├── app │ ├── web │ │ ├── .dockerignore │ │ ├── .stylelintignore │ │ ├── src │ │ │ ├── md │ │ │ │ └── law │ │ │ │ │ ├── 品牌使用準則.md │ │ │ │ │ └── 看板和活動政策.md │ │ │ ├── tsx │ │ │ │ ├── party │ │ │ │ │ └── index.tsx │ │ │ │ ├── components │ │ │ │ │ ├── invalid_message.tsx │ │ │ │ │ ├── number_over.tsx │ │ │ │ │ ├── scalable_input.tsx │ │ │ │ │ ├── drop_down.tsx │ │ │ │ │ └── modal_window.tsx │ │ │ │ ├── global_state │ │ │ │ │ ├── draft.tsx │ │ │ │ │ ├── config.tsx │ │ │ │ │ ├── subscribed_boards.tsx │ │ │ │ │ ├── user.tsx │ │ │ │ │ ├── editor_panel.tsx │ │ │ │ │ ├── location.tsx │ │ │ │ │ └── notification.tsx │ │ │ │ ├── display │ │ │ │ │ ├── show_text.tsx │ │ │ │ │ ├── show_pure_text.tsx │ │ │ │ │ └── show_markdown.tsx │ │ │ │ ├── bottom_panel.tsx │ │ │ │ ├── law_page.tsx │ │ │ │ ├── profile │ │ │ │ │ └── user_card.tsx │ │ │ │ ├── mobile │ │ │ │ │ └── app.tsx │ │ │ │ ├── verify_title.tsx │ │ │ │ ├── app.tsx │ │ │ │ ├── change_email.tsx │ │ │ │ ├── app │ │ │ │ │ ├── providers.tsx │ │ │ │ │ └── init.ts │ │ │ │ ├── header │ │ │ │ │ ├── notification.tsx │ │ │ │ │ └── search_bar.tsx │ │ │ │ ├── subscribe_article_page.tsx │ │ │ │ ├── pop_article_page.tsx │ │ │ │ └── article_card │ │ │ │ │ └── bonder.tsx │ │ │ ├── css │ │ │ │ ├── components │ │ │ │ │ ├── invalid_message.module.css │ │ │ │ │ ├── number_over.module.css │ │ │ │ │ ├── comment_editor.module.css │ │ │ │ │ ├── drop_down.module.css │ │ │ │ │ ├── select.module.css │ │ │ │ │ ├── modal_window.module.css │ │ │ │ │ ├── tab_panel.module.css │ │ │ │ │ └── dual_slider.module.css │ │ │ │ ├── board │ │ │ │ │ ├── bonder.module.css │ │ │ │ │ ├── graph_view.module.css │ │ │ │ │ ├── article_page.module.css │ │ │ │ │ ├── board_page.module.css │ │ │ │ │ └── board_editor.module.css │ │ │ │ ├── article_wrapper.module.css │ │ │ │ ├── pop_article_page.module.css │ │ │ │ ├── law_page.module.css │ │ │ │ ├── global.css │ │ │ │ ├── signup_invitation_page.module.css │ │ │ │ ├── reset_password.module.css │ │ │ │ ├── invite_page.module.css │ │ │ │ ├── party │ │ │ │ │ ├── party_detail.module.css │ │ │ │ │ └── my_party_list.module.css │ │ │ │ ├── header │ │ │ │ │ ├── search_bar.module.css │ │ │ │ │ └── login_modal.module.css │ │ │ │ ├── custom.d.ts │ │ │ │ ├── avatar.module.css │ │ │ │ ├── signup_page.module.css │ │ │ │ ├── notification.module.css │ │ │ │ ├── markdown.css │ │ │ │ ├── setting_page.module.css │ │ │ │ ├── layout.css │ │ │ │ ├── board_list.module.css │ │ │ │ ├── left_panel │ │ │ │ │ ├── browse_bar.module.css │ │ │ │ │ ├── draft_bar.module.css │ │ │ │ │ ├── chat_bar.module.css │ │ │ │ │ └── left_panel.module.css │ │ │ │ ├── bottom_panel │ │ │ │ │ └── bottom_panel.module.css │ │ │ │ ├── mobile │ │ │ │ │ └── panel.module.css │ │ │ │ ├── user_card.module.css │ │ │ │ └── variable.css │ │ │ ├── img │ │ │ │ ├── text.png │ │ │ │ ├── title │ │ │ │ │ ├── 律師.png │ │ │ │ │ └── 站方代表.png │ │ │ │ ├── icon-128x128.png │ │ │ │ ├── icon-192x192.png │ │ │ │ ├── icon-512x512.png │ │ │ │ └── icon.svg │ │ │ ├── ts │ │ │ │ ├── constants.ts │ │ │ │ ├── sound.ts │ │ │ │ ├── regex_util.ts │ │ │ │ ├── utils.ts │ │ │ │ └── date.ts │ │ │ └── sound │ │ │ │ └── message-notification.mp3 │ │ ├── public │ │ │ ├── robots.txt │ │ │ ├── no-avatar.png │ │ │ ├── icon-192x192.png │ │ │ ├── icon-512x512.png │ │ │ ├── manifest.json │ │ │ └── icon.svg │ │ ├── .eslintrc │ │ ├── deploy │ │ │ ├── Dockerfile │ │ │ └── nginx.conf │ │ ├── .gitignore │ │ ├── .gitattributes │ │ ├── stylelint.config.js │ │ ├── postcss.config.js │ │ ├── jest.config.js │ │ ├── vite_plugins │ │ │ └── mobile.ts │ │ ├── index.html │ │ ├── index.mobile.html │ │ ├── tsconfig.json │ │ ├── test │ │ │ ├── tsx │ │ │ │ └── profile │ │ │ │ │ └── user_card.test.tsx │ │ │ └── ts │ │ │ │ └── date.test.ts │ │ ├── vite.config.ts │ │ └── package.json │ └── README.md ├── lib │ └── api │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── index.ts │ │ ├── webhook_type.ts │ │ └── api_utils.ts ├── .gitignore ├── script │ └── inject │ │ ├── package.json │ │ ├── data │ │ ├── array.json │ │ ├── crouching_dragon.json │ │ ├── graph.json │ │ └── superlong.json │ │ └── tsconfig.json ├── package.json └── .eslintrc ├── api-service ├── src │ ├── notification │ │ └── mod.rs │ ├── chat │ │ └── mod.rs │ ├── api │ │ ├── model │ │ │ ├── mod.rs │ │ │ ├── webhook.rs │ │ │ └── chat.rs │ │ └── mod.rs │ ├── service │ │ ├── mod.rs │ │ ├── hot_articles.rs │ │ └── graph_view.rs │ ├── util │ │ ├── mod.rs │ │ ├── token.rs │ │ ├── board.rs │ │ └── article.rs │ ├── service_manager.rs │ ├── config │ │ └── mod.rs │ ├── redis │ │ └── mod.rs │ ├── lib.rs │ ├── email │ │ ├── mailgun.rs │ │ └── smtp.rs │ ├── db │ │ ├── favorite.rs │ │ ├── tracking.rs │ │ ├── mod.rs │ │ ├── avatar.rs │ │ └── subscribed_boards.rs │ └── bin │ │ ├── main.rs │ │ └── prepare.rs ├── config │ ├── secret │ │ └── MAILGUN_KEY.example │ ├── .gitignore │ ├── test_data.txt │ ├── carbonbond.docker.toml │ └── carbonbond.toml ├── migrations │ ├── 20220507063524_draft_fields.sql │ ├── 20210822141335_rename_code_to_token.sql │ ├── 20220410164129_add_comment_replied_notification.sql │ ├── 20220411152119_anonymous_comment.sql │ ├── 20230319080114_add_mentioned_in_comment_notification.sql │ ├── 20220528083013_add_other_comment_replied_notification.sql │ ├── 20211030102047_article_chat.sql │ ├── 20220205083635_user_birth_year_column.sql │ ├── 20211012052001_anonymous_author.sql │ ├── 20220128090622_direct_chat_read_time.sql │ ├── 20220523124855_change_email.sql │ ├── 20220514141235_unique_chat.sql │ ├── 20211007153952_create_tracking_article.sql │ ├── 20211210123926_lawyer_certificate.sql │ ├── 20220502083842_attidude_to_article.sql │ ├── 20210910103837_draft.sql │ ├── 20220301142008_comment.sql │ ├── 20210818091002_invitation_credits.sql │ ├── 20220704091538_claim_title_token.sql │ ├── 20230226161606_robots.sql │ ├── 20220207115827_simplify_force.sql.sql │ ├── 20220213154105_add_title_and_certification_table.sql │ ├── 20211123121445_introduce_private_relations.sql │ ├── 20201108205511_notification.sql │ └── 20200819114815_chat_schema.sql ├── assets │ └── no-avatar.png ├── .dockerignore ├── .gitignore ├── build.sh ├── deploy │ └── Dockerfile └── Cargo.toml ├── .gitignore ├── .github └── workflows │ ├── rust-dummy.yml │ ├── node.js-dummy.yml │ ├── node.js.yml │ ├── api-service-build-production.yml │ ├── frontend-build-production.yml │ ├── api-service-build.yml │ ├── frontend-build.yml │ └── rust.yml ├── mac-run.sh ├── run.sh └── README.md /deploy/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /docs-website/docs/開發/本地運行.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs-website/docs/基礎/設定檔一覽.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs-website/docs/開發/環境設定.md: -------------------------------------------------------------------------------- 1 | # 環境設定 -------------------------------------------------------------------------------- /frontend/app/web/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /docs-website/docs/基礎/碳鍵是什麼.md: -------------------------------------------------------------------------------- 1 | # 碳鍵是什麼? 2 | 3 | 撰寫中 -------------------------------------------------------------------------------- /api-service/src/notification/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod service; 2 | -------------------------------------------------------------------------------- /frontend/app/web/.stylelintignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | -------------------------------------------------------------------------------- /frontend/app/web/src/md/law/品牌使用準則.md: -------------------------------------------------------------------------------- 1 | # 品牌使用準則 2 | TODO: 撰寫本準則 -------------------------------------------------------------------------------- /api-service/config/secret/MAILGUN_KEY.example: -------------------------------------------------------------------------------- 1 | my-super-mail-gun-api -------------------------------------------------------------------------------- /frontend/app/web/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /frontend/app/web/src/md/law/看板和活動政策.md: -------------------------------------------------------------------------------- 1 | # 看板和活動政策 2 | 3 | TODO: 撰寫本政策 -------------------------------------------------------------------------------- /api-service/src/chat/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod chat; 2 | pub mod message; 3 | pub mod service; 4 | -------------------------------------------------------------------------------- /frontend/app/web/src/tsx/party/index.tsx: -------------------------------------------------------------------------------- 1 | export const EXILED_PARTY_NAME = '流亡政黨'; 2 | -------------------------------------------------------------------------------- /api-service/src/api/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod chat; 2 | pub mod forum; 3 | pub mod webhook; 4 | -------------------------------------------------------------------------------- /docs-website/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | docs/.vitepress/cache 3 | docs/.vitepress/dist 4 | 5 | -------------------------------------------------------------------------------- /deploy/build.sh: -------------------------------------------------------------------------------- 1 | cd ../api-service 2 | sh build.sh 3 | cd ../frontend/app/web 4 | yarn && yarn build 5 | -------------------------------------------------------------------------------- /api-service/migrations/20220507063524_draft_fields.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE drafts 2 | ADD fields TEXT NOT NULL DEFAULT '[]'; -------------------------------------------------------------------------------- /frontend/app/web/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "react/react-in-jsx-scope": "error", 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/app/web/src/css/components/invalid_message.module.css: -------------------------------------------------------------------------------- 1 | .invalidMessage { 2 | color: var(--red); 3 | } 4 | -------------------------------------------------------------------------------- /api-service/assets/no-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/api-service/assets/no-avatar.png -------------------------------------------------------------------------------- /api-service/migrations/20210822141335_rename_code_to_token.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE reset_password 2 | RENAME COLUMN code to token; -------------------------------------------------------------------------------- /frontend/lib/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "carbonbond-api", 3 | "version": "0.1.0", 4 | "license": "MIT" 5 | } 6 | -------------------------------------------------------------------------------- /docs-website/docs/早期文件/img/鍵結關係.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/docs-website/docs/早期文件/img/鍵結關係.png -------------------------------------------------------------------------------- /frontend/app/web/src/img/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/frontend/app/web/src/img/text.png -------------------------------------------------------------------------------- /api-service/.dockerignore: -------------------------------------------------------------------------------- 1 | # 忽略所有檔案 2 | * 3 | 4 | # 以下為白名單 5 | !target/release/server 6 | !config/ 7 | !migrations/ 8 | !assets/ 9 | -------------------------------------------------------------------------------- /api-service/migrations/20220410164129_add_comment_replied_notification.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE notification_kind ADD VALUE 'comment_replied'; -------------------------------------------------------------------------------- /api-service/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api_impl; 2 | #[rustfmt::skip] 3 | pub mod api_trait; 4 | pub mod model; 5 | pub mod query; 6 | -------------------------------------------------------------------------------- /api-service/src/service/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod graph_view; 2 | pub mod hot_articles; 3 | pub mod hot_boards; 4 | pub mod notification; 5 | -------------------------------------------------------------------------------- /docs-website/docs/早期文件/img/ptt討論串.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/docs-website/docs/早期文件/img/ptt討論串.png -------------------------------------------------------------------------------- /docs-website/docs/早期文件/img/課程評價表單.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/docs-website/docs/早期文件/img/課程評價表單.png -------------------------------------------------------------------------------- /frontend/app/web/public/no-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/frontend/app/web/public/no-avatar.png -------------------------------------------------------------------------------- /frontend/app/web/src/img/title/律師.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/frontend/app/web/src/img/title/律師.png -------------------------------------------------------------------------------- /docs-website/docs/早期文件/img/ptt課程評價_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/docs-website/docs/早期文件/img/ptt課程評價_1.png -------------------------------------------------------------------------------- /docs-website/docs/早期文件/img/ptt課程評價_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/docs-website/docs/早期文件/img/ptt課程評價_2.png -------------------------------------------------------------------------------- /docs-website/docs/早期文件/img/ptt課程評價模板.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/docs-website/docs/早期文件/img/ptt課程評價模板.png -------------------------------------------------------------------------------- /frontend/app/web/public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/frontend/app/web/public/icon-192x192.png -------------------------------------------------------------------------------- /frontend/app/web/public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/frontend/app/web/public/icon-512x512.png -------------------------------------------------------------------------------- /frontend/app/web/src/img/title/站方代表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/frontend/app/web/src/img/title/站方代表.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # editor 2 | *.swo 3 | *.swp 4 | .vscode/ 5 | *~ 6 | .*~ 7 | 8 | # log 9 | **/*.log 10 | 11 | # rust 12 | target/ 13 | -------------------------------------------------------------------------------- /api-service/migrations/20220411152119_anonymous_comment.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE comments 2 | ADD anonymous boolean NOT NULL 3 | DEFAULT (false); 4 | -------------------------------------------------------------------------------- /api-service/migrations/20230319080114_add_mentioned_in_comment_notification.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE notification_kind ADD VALUE 'mentioned_in_comment'; -------------------------------------------------------------------------------- /frontend/app/web/src/img/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/frontend/app/web/src/img/icon-128x128.png -------------------------------------------------------------------------------- /frontend/app/web/src/img/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/frontend/app/web/src/img/icon-192x192.png -------------------------------------------------------------------------------- /frontend/app/web/src/img/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/frontend/app/web/src/img/icon-512x512.png -------------------------------------------------------------------------------- /api-service/.gitignore: -------------------------------------------------------------------------------- 1 | # build 2 | /target 3 | **/*.rs.bk 4 | 5 | # db 6 | data/** 7 | 8 | # temp files 9 | .db_tool_history 10 | 11 | -------------------------------------------------------------------------------- /api-service/migrations/20220528083013_add_other_comment_replied_notification.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE notification_kind ADD VALUE 'other_comment_replied'; 2 | -------------------------------------------------------------------------------- /api-service/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | mod board; 2 | pub use board::*; 3 | 4 | mod article; 5 | pub use article::*; 6 | 7 | mod token; 8 | pub use token::*; 9 | -------------------------------------------------------------------------------- /frontend/app/web/deploy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | COPY dist /usr/share/nginx/html 4 | COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf -------------------------------------------------------------------------------- /frontend/app/web/src/ts/constants.ts: -------------------------------------------------------------------------------- 1 | export const STORAGE_NAME = { 2 | browsebar_expand: 'browsebar:expand', 3 | leftbar_expand: 'leftbar:expand' 4 | }; -------------------------------------------------------------------------------- /api-service/migrations/20211030102047_article_chat.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE chat.direct_chats 2 | ADD article_id bigint REFERENCES articles (id) NULL 3 | DEFAULT (NULL); -------------------------------------------------------------------------------- /frontend/app/web/src/sound/message-notification.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carbon-bond/carbonbond/HEAD/frontend/app/web/src/sound/message-notification.mp3 -------------------------------------------------------------------------------- /api-service/migrations/20220205083635_user_birth_year_column.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | 3 | ALTER TABLE users 4 | ADD birth_year int NOT NULL 5 | DEFAULT (0); 6 | -------------------------------------------------------------------------------- /frontend/app/README.md: -------------------------------------------------------------------------------- 1 | 本目錄放置不同平臺的碳鍵前端。 2 | 3 | 目前僅支援 web 平臺,若未來打算支援原生 android/ios ,或是命令行界面,可分別創建 4 | 5 | - android 6 | - ios 7 | - cli 8 | 9 | 等等目錄,並將相關程式碼置於其中。 10 | -------------------------------------------------------------------------------- /frontend/lib/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "skipLibCheck": true, 6 | "noEmit": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/app/web/src/css/board/bonder.module.css: -------------------------------------------------------------------------------- 1 | .replyButtons { 2 | font-size: 16px; 3 | & .replyButton { 4 | padding: 1px 3px; 5 | margin-right: 5px; 6 | } 7 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # node 2 | node_modules/ 3 | 4 | # chitin-codegen 5 | api_trait.rs 6 | 7 | # 編譯產物 8 | dist/ 9 | 10 | # jest coverage 11 | coverage/ 12 | 13 | # eslint 14 | .eslintcache 15 | -------------------------------------------------------------------------------- /api-service/config/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !secret/ 3 | secret/* 4 | !secret/*.example 5 | 6 | !.gitignore 7 | !carbonbond.toml 8 | !carbonbond.docker.toml 9 | !default_category.json 10 | !test_data.txt 11 | -------------------------------------------------------------------------------- /api-service/migrations/20211012052001_anonymous_author.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE articles 2 | ADD anonymous boolean NOT NULL 3 | DEFAULT (false); 4 | 5 | ALTER TABLE drafts 6 | ADD anonymous boolean NOT NULL 7 | DEFAULT (false); -------------------------------------------------------------------------------- /frontend/app/web/.gitignore: -------------------------------------------------------------------------------- 1 | # node 2 | node_modules/ 3 | 4 | # chitin-codegen 5 | api_trait.rs 6 | 7 | # 編譯產物 8 | dist/ 9 | 10 | # jest coverage 11 | coverage/ 12 | 13 | # eslint 14 | .eslintcache 15 | -------------------------------------------------------------------------------- /frontend/app/web/.gitattributes: -------------------------------------------------------------------------------- 1 | src/ts/api/api_trait.ts linguist-generated=true 2 | src/ts/api/api_trait.ts -diff -merge 3 | 4 | src/ts/api/webhook_type.ts linguist-generated=true 5 | src/ts/api/webhook_type.ts -diff -merge -------------------------------------------------------------------------------- /frontend/lib/api/index.ts: -------------------------------------------------------------------------------- 1 | import * as api_trait from './api_trait'; 2 | import * as api_utils from './api_utils'; 3 | import * as webhook_type from './webhook_type'; 4 | 5 | export { api_trait, api_utils, webhook_type }; 6 | -------------------------------------------------------------------------------- /frontend/app/web/src/css/article_wrapper.module.css: -------------------------------------------------------------------------------- 1 | .articleWrapper { 2 | padding: 10px 0 10px 0; 3 | display: block; 4 | position: relative; 5 | } 6 | .articleWrapper:not(:first-child) { 7 | padding: 0px 0 10px 0; 8 | } -------------------------------------------------------------------------------- /frontend/app/web/src/css/pop_article_page.module.css: -------------------------------------------------------------------------------- 1 | .articleWrapper { 2 | padding: 10px 0 10px 0; 3 | display: block; 4 | position: relative; 5 | } 6 | .articleWrapper:not(:first-child) { 7 | padding: 0px 0 10px 0; 8 | } -------------------------------------------------------------------------------- /frontend/app/web/stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'selector-class-pattern': '^[a-z]+([A-Z][a-z]+)*$', 4 | 'selector-id-pattern': '^[a-z]+([A-Z][a-z]+)*$', 5 | }, 6 | ignoreFiles: ['./dist/**/*.css'] 7 | }; -------------------------------------------------------------------------------- /frontend/app/web/src/ts/sound.ts: -------------------------------------------------------------------------------- 1 | import message_notification_url from '../sound/message-notification.mp3'; 2 | 3 | export function play_message_notification(): void { 4 | const audio = new Audio(message_notification_url); 5 | audio.play(); 6 | } -------------------------------------------------------------------------------- /frontend/lib/api/webhook_type.ts: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | export type MentionInComment = { article_id: number; comment_id: number; author_id: number; comment_content: string }; 3 | export type API = 4 | | { MentionedInComment: MentionInComment }; 5 | -------------------------------------------------------------------------------- /frontend/app/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-preset-env': { 4 | features: { 5 | 'nesting-rules': true 6 | } 7 | }, // 使用仍在 stage 的 CSS 特性 8 | 'autoprefixer': {}, // 加入各家瀏覽器的前綴詞 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /api-service/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | export SQLX_OFFLINE=true 4 | 5 | cargo build --release --bin server 6 | echo "執行檔大小" 7 | ls -sh "./target/release/server" 8 | 9 | strip ./target/release/server 10 | echo "strip 之後" 11 | ls -sh "./target/release/server" 12 | -------------------------------------------------------------------------------- /api-service/config/test_data.txt: -------------------------------------------------------------------------------- 1 | reset 2 | add user a@carbonbond.com a 1 3 | add user b@carbonbond.com b 1 4 | add user 金剛@carbonbond.com 金剛 1 5 | as a 6 | add board 共產國際 我們的板 7 | add party 流浪的人 8 | add direct_chat 1 2 9 | add direct_chat 1 3 10 | add direct_msg 1 你好,我是a先生 11 | add direct_msg 2 yo!金剛 -------------------------------------------------------------------------------- /frontend/app/web/src/css/law_page.module.css: -------------------------------------------------------------------------------- 1 | .lawPage { 2 | margin-top: 20px; 3 | display: flex; 4 | justify-content: center; 5 | & div { 6 | max-width: 800px; 7 | line-height: 24px; 8 | & h1 { 9 | margin-bottom: 40px; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /api-service/migrations/20220128090622_direct_chat_read_time.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | 3 | ALTER TABLE chat.direct_chats 4 | ADD read_time_1 timestamptz NOT NULL DEFAULT NOW(), 5 | ADD read_time_2 timestamptz NOT NULL DEFAULT NOW(), 6 | ADD last_message bigint REFERENCES chat.direct_messages (id) NULL; 7 | -------------------------------------------------------------------------------- /frontend/app/web/src/css/components/number_over.module.css: -------------------------------------------------------------------------------- 1 | .numberOver { 2 | position: relative; 3 | & .number { 4 | position: absolute; 5 | top: 0px; 6 | color: var(--white); 7 | font-size: 14px; 8 | background-color: var(--red); 9 | padding: 1px 5px; 10 | border-radius: 10px; 11 | } 12 | } -------------------------------------------------------------------------------- /frontend/app/web/src/tsx/components/invalid_message.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import style from '../../css/components/invalid_message.module.css'; 3 | 4 | export function InvalidMessage(props: { msg: string }): JSX.Element { 5 | return ⚠ {props.msg}; 6 | } -------------------------------------------------------------------------------- /api-service/migrations/20220523124855_change_email.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE change_email ( 2 | id bigserial PRIMARY KEY, 3 | token varchar(32) NOT NULL, 4 | user_id bigint REFERENCES users (id) NOT NULL, 5 | create_time timestamptz NOT NULL DEFAULT NOW(), 6 | is_used boolean NOT NULL DEFAULT FALSE, 7 | email text 8 | ); -------------------------------------------------------------------------------- /.github/workflows/rust-dummy.yml: -------------------------------------------------------------------------------- 1 | name: Rust (dummy) 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | paths-ignore: 7 | - 'api-service/**' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - name: 假的測試 14 | run: echo "dummy backend CI (always pass)" 15 | 16 | -------------------------------------------------------------------------------- /api-service/migrations/20220514141235_unique_chat.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE chat.direct_chats 2 | ADD UNIQUE (user_id_1, user_id_2, article_id); 3 | 4 | CREATE UNIQUE INDEX unique_direct_chats ON chat.direct_chats (user_id_1, user_id_2) 5 | WHERE article_id IS NULL; 6 | 7 | ALTER TABLE chat.direct_chats 8 | ADD CHECK (user_id_1 < user_id_2); -------------------------------------------------------------------------------- /.github/workflows/node.js-dummy.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI (dummy) 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | paths-ignore: 7 | - 'frontend/**' 8 | 9 | jobs: 10 | frontend-build: 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - name: 假的測試 14 | run: echo "dummy frontend CI (always pass)" 15 | 16 | -------------------------------------------------------------------------------- /api-service/migrations/20211007153952_create_tracking_article.sql: -------------------------------------------------------------------------------- 1 | -- 追蹤 2 | CREATE TABLE tracking_articles ( 3 | id bigserial PRIMARY KEY, 4 | user_id bigint REFERENCES users (id) NOT NULL, 5 | article_id bigint REFERENCES articles (id) NOT NULL, 6 | create_time timestamptz NOT NULL DEFAULT NOW(), 7 | UNIQUE (user_id, article_id) 8 | ); 9 | -------------------------------------------------------------------------------- /api-service/migrations/20211210123926_lawyer_certificate.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | ALTER TABLE signup_tokens 3 | ADD gender text NOT NULL 4 | DEFAULT ('other'); 5 | 6 | ALTER TABLE signup_tokens 7 | ADD birth_year int NOT NULL 8 | DEFAULT (1900); 9 | 10 | ALTER TABLE signup_tokens 11 | ADD license_id text NOT NULL 12 | DEFAULT (''); 13 | -------------------------------------------------------------------------------- /docs-website/README.md: -------------------------------------------------------------------------------- 1 | ## 查看 2 | 3 | [點此](https://docs.carbonbond.cc)線上查看。 4 | 5 | 如果您想要離線查看,可在本資料夾執行 6 | ``` 7 | yarn 8 | yarn dev 9 | ``` 10 | 即可在 http://localhost:5173 查看文件。 11 | 12 | ## 編輯 13 | 執行 `yarn dev` 之後, 對本文件修改會自動熱更新到 localhost:5173 ,可以方便地預覽 Markdown 渲染結果。 14 | 15 | 本文件由 [vitepress](https://vitepress.vuejs.org/) 生成,可參考其[文件](https://vitepress.vuejs.org/)進行設置。 -------------------------------------------------------------------------------- /mac-run.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | export NAME="cb/run" 3 | 4 | brew services start redis 5 | brew services start postgresql 6 | 7 | cd api-service 8 | tmux new-session -s $NAME -d "env RUST_LOG=debug cargo run; bash" 9 | cd ../frontend/app/web 10 | tmux split-window -h "yarn dev; bash" 11 | tmux split-window -v "yarn check-ts --watch; bash" 12 | 13 | tmux -2 attach-session -d 14 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | export NAME="cb/run" 3 | 4 | sudo service redis-server start 5 | sudo service postgresql start 6 | 7 | cd api-service 8 | tmux new-session -s $NAME -d "env RUST_LOG=debug cargo run; bash" 9 | cd ../frontend/app/web 10 | tmux split-window -h "yarn dev; bash" 11 | tmux split-window -v "yarn check-ts --watch; bash" 12 | 13 | tmux -2 attach-session -d 14 | -------------------------------------------------------------------------------- /frontend/app/web/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | moduleNameMapper: { 6 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/__mocks__/fileMock.js', 7 | '\\.(scss|sass|css)$': 'identity-obj-proxy' 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /api-service/migrations/20220502083842_attidude_to_article.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE attitude_to_articles ( 2 | id bigserial PRIMARY KEY, 3 | user_id bigint REFERENCES users (id) NOT NULL, 4 | article_id bigint REFERENCES articles (id) NOT NULL, 5 | attitude BOOLEAN NOT NULL, 6 | UNIQUE (user_id, article_id) 7 | ); 8 | 9 | ALTER TABLE articles 10 | ADD good bigint NOT NULL DEFAULT (0), 11 | ADD bad bigint NOT NULL DEFAULT (0); -------------------------------------------------------------------------------- /api-service/deploy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | WORKDIR '/app/api-service' 4 | RUN apt-get update && \ 5 | apt-get install -y --no-install-recommends ca-certificates && \ 6 | apt-get clean && \ 7 | apt-get autoremove && \ 8 | rm -rf /var/lib/apt/lists/* /var/tmp/* /tmp/* 9 | COPY target/release/server . 10 | COPY assets ./assets 11 | COPY migrations ./migrations 12 | ENV RUST_LOG debug 13 | ENV MODE release 14 | CMD ./server -------------------------------------------------------------------------------- /api-service/migrations/20210910103837_draft.sql: -------------------------------------------------------------------------------- 1 | -- 草稿 2 | CREATE TABLE drafts ( 3 | id bigserial PRIMARY KEY, 4 | author_id bigint REFERENCES users (id) NOT NULL, 5 | board_id bigint REFERENCES boards (id) NOT NULL, 6 | category_id bigint REFERENCES categories (id), 7 | title text NOT NULL, 8 | content text NOT NULL, 9 | create_time timestamptz NOT NULL DEFAULT NOW(), 10 | edit_time timestamptz NOT NULL DEFAULT NOW() 11 | ); -------------------------------------------------------------------------------- /frontend/script/inject/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inject", 3 | "version": "0.1.0", 4 | "description": "用於注入碳鍵測試用的看板、文章", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "inject": "ts-node main.ts" 9 | }, 10 | "devDependencies": { 11 | "carbonbond-api": "0.1.0", 12 | "@types/minimist": "^1.2.2", 13 | "@types/prompts": "^2.4.2", 14 | "minimist": "^1.2.8", 15 | "prompts": "^2.4.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/app/web/src/css/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | text-decoration: none; 5 | outline: none; 6 | box-sizing: border-box; 7 | } 8 | body { 9 | margin: 0; 10 | height: 100%; 11 | width: 100%; 12 | background-color: var(--lower-background-color); 13 | } 14 | a:not(.styleless) { 15 | color: var(--gray-5); 16 | } 17 | a:not(.styleless):hover { 18 | color: var(--link-color); 19 | text-decoration: underline; 20 | } -------------------------------------------------------------------------------- /docs-website/docs/進階/架構.md: -------------------------------------------------------------------------------- 1 | # 架構 2 | 3 | 碳鍵的 [docker-compose.yml](https://github.com/carbon-bond/carbonbond/blob/master/deploy/docker-compose.yml) 設定中包含四個 docker 容器,分別是 postgres, redis, frontend, api-service。 4 | 5 | 其中的 frontend,是 Nginx 官方容器加上靜態檔案以及 [Nginx 設定檔](https://github.com/carbon-bond/carbonbond/blob/master/frontend/app/web/deploy/nginx.conf),它監聽於 80 埠口,是瀏覽器直接接觸的容器,當收到請求時,frontend 會做兩件事情: 6 | 7 | - 若是靜態檔案請求,返回檔案內容 8 | - 若是 API 請求,將請求反向代理給 api-service 9 | 10 | ![架構](./架構.svg) -------------------------------------------------------------------------------- /frontend/app/web/src/css/signup_invitation_page.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: 20px; 3 | margin-top: 30px; 4 | margin-bottom: 12px; 5 | } 6 | .invitationList { 7 | margin-top: 30px; 8 | & .invitation { 9 | border: 1px solid var(--border-color); 10 | & .email { 11 | color: blue; 12 | } 13 | & .isUsed { 14 | margin-left: 20px; 15 | } 16 | } 17 | } 18 | .creditList { 19 | margin-top: 30px; 20 | & .credit { 21 | border: 1px solid var(--border-color); 22 | } 23 | } -------------------------------------------------------------------------------- /api-service/migrations/20220301142008_comment.sql: -------------------------------------------------------------------------------- 1 | -- 留言 2 | CREATE TABLE comments ( 3 | id bigserial PRIMARY KEY, 4 | author_id bigint REFERENCES users (id) NOT NULL, 5 | content text NOT NULL, 6 | article_id bigint REFERENCES articles (id) NOT NULL, 7 | attached_comment_id bigint REFERENCES comments(id), 8 | create_time timestamptz NOT NULL DEFAULT NOW(), 9 | edit_time timestamptz NOT NULL DEFAULT NOW() 10 | ); 11 | 12 | CREATE INDEX comments_article_index ON comments (article_id); -------------------------------------------------------------------------------- /docs-website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "carbonbond-docs", 3 | "version": "0.1.0", 4 | "description": "碳鍵論壇的文件網站", 5 | "author": "金剛", 6 | "license": "ISC", 7 | "scripts": { 8 | "dev": "vitepress dev docs", 9 | "build": "vitepress build docs", 10 | "preview": "vitepress preview docs" 11 | }, 12 | "devDependencies": { 13 | "@types/node": "^18.14.0", 14 | "markdown-it-footnote": "^3.0.3", 15 | "vitepress": "^1.0.0-alpha.47", 16 | "vue": "^3.2.47" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/script/inject/data/array.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "一堆陣列", 4 | "force": [ 5 | "文章 {", 6 | "鍵結[*] 鍵結們[0~2]", 7 | "數字 數字們[2~4]", 8 | "單行 單行們[1~2]", 9 | "文本 文本們[1~2]", 10 | "}" 11 | ], 12 | "articles": [{ 13 | "title": "不知道打什麼纔好", 14 | "category": "文章", 15 | "content": { 16 | "鍵結們": [], 17 | "數字們": [1,2], 18 | "單行們": ["abcd"], 19 | "文本們": ["abcd"] 20 | } 21 | } 22 | ] 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /frontend/app/web/src/css/components/comment_editor.module.css: -------------------------------------------------------------------------------- 1 | .editor { 2 | min-height: 70px !important; 3 | background: var(--white); 4 | color: var(--font-color); 5 | line-height: 20px; 6 | font-weight: 400; 7 | text-align: left; 8 | border: 1px solid var(--gray-3); 9 | } 10 | 11 | .mention { 12 | padding: 1px 1px; 13 | margin: 0px 3px; 14 | display: inline-block; 15 | border-radius: 4px; 16 | background-color: var(--third-theme-color); 17 | fontSize: 0.9em; 18 | border: 1px solid black; 19 | } -------------------------------------------------------------------------------- /api-service/src/service_manager.rs: -------------------------------------------------------------------------------- 1 | use crate::chat::service::ChatService; 2 | use crate::notification::service::NotificationService; 3 | 4 | pub struct ServiceManager { 5 | pub notification_service: NotificationService, 6 | pub chat_service: ChatService, 7 | } 8 | 9 | impl ServiceManager { 10 | pub async fn new() -> Self { 11 | ServiceManager { 12 | notification_service: NotificationService::new().await, 13 | chat_service: ChatService::default(), 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/app/web/src/css/reset_password.module.css: -------------------------------------------------------------------------------- 1 | .signupPage { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: center; 5 | & .signupForm { 6 | width: 40%; 7 | margin-top: 8%; 8 | & .counter { 9 | margin-bottom: 20px; 10 | } 11 | & .username { 12 | width: 100%; 13 | margin-bottom: 10px; 14 | } 15 | & .password { 16 | width: 100%; 17 | margin-bottom: 10px; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /frontend/app/web/src/tsx/global_state/draft.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | const { useState } = React; 3 | import { createContainer } from 'unstated-next'; 4 | import { Draft } from 'carbonbond-api/api_trait'; 5 | 6 | 7 | function useDraftState(): { 8 | setDraftData: (data: Draft[]) => void, 9 | draft_data: Draft[], 10 | } { 11 | let [draft_data, setDraftData] = useState([]); 12 | 13 | return { 14 | draft_data, 15 | setDraftData 16 | }; 17 | } 18 | 19 | export const DraftState = createContainer(useDraftState); -------------------------------------------------------------------------------- /frontend/app/web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "碳鍵", 3 | "short_name": "碳鍵", 4 | "start_url": "/app", 5 | "theme_color": "#e51a30", 6 | "background_color": "#fafafa", 7 | "icons": [ 8 | { 9 | "src": "icon-192x192.png", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "icon-512x512.png", 15 | "sizes": "512x512", 16 | "type": "image/png" 17 | } 18 | ], 19 | "display": "standalone" 20 | } -------------------------------------------------------------------------------- /api-service/migrations/20210818091002_invitation_credits.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE invitations; 2 | DROP TABLE signup_invitations; 3 | 4 | -- 邀請額度 5 | CREATE TABLE invitation_credits ( 6 | id bigserial PRIMARY KEY, 7 | user_id bigint REFERENCES users (id) NOT NULL, 8 | event_name TEXT NOT NULL, 9 | credit int NOT NULL, 10 | create_time timestamptz NOT NULL DEFAULT NOW() 11 | ); 12 | 13 | ALTER TABLE signup_tokens 14 | ADD COLUMN inviter_id bigint REFERENCES users (id), -- 發出邀請者的 id ,若自行註冊,其值爲 NULL 15 | ADD COLUMN is_used boolean NOT NULL DEFAULT FALSE; 16 | -------------------------------------------------------------------------------- /api-service/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use state::LocalStorage; 2 | 3 | mod config; 4 | pub use config::*; 5 | 6 | static CONFIG: LocalStorage = LocalStorage::new(); 7 | 8 | /// 載入設定檔,將設定檔物件儲存於全域狀態 9 | /// * `paths` 一至多個檔案路徑,函式會選擇第一個讀取成功的設定檔 10 | pub fn init(path: Option) { 11 | let config = load_config(&path).unwrap(); 12 | log::info!("初始化設定檔:{:?}", config.file_name); 13 | assert!(CONFIG.set(move || config.clone()), "init() is called twice",); 14 | } 15 | 16 | pub fn get_config() -> &'static Config { 17 | CONFIG.get() 18 | } 19 | -------------------------------------------------------------------------------- /frontend/app/web/src/css/invite_page.module.css: -------------------------------------------------------------------------------- 1 | .invitePage { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: center; 5 | & .inviteForm { 6 | width: 40%; 7 | margin-top: 8%; 8 | & .counter { 9 | margin-bottom: 20px; 10 | } 11 | & .email { 12 | width: 100%; 13 | margin-bottom: 10px; 14 | } 15 | & .invitation { 16 | width: 100%; 17 | height: 80px; 18 | margin-bottom: 10px; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /frontend/app/web/src/tsx/display/show_text.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ShowMarkdown } from './show_markdown'; 3 | import { ShowPureText } from './show_pure_text'; 4 | 5 | export enum Format { 6 | PureText, 7 | Markdown, 8 | } 9 | 10 | export function ShowText(props: { 11 | text: string, 12 | format: Format 13 | }): JSX.Element { 14 | switch (props.format) { 15 | case Format.PureText: 16 | return ; 17 | case Format.Markdown: 18 | return ; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /api-service/migrations/20220704091538_claim_title_token.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE claim_title_tokens ( 2 | user_id bigint REFERENCES users (id) NOT NULL, 3 | token text NOT NULL PRIMARY KEY, 4 | title text NOT NULL, 5 | license_id text NOT NULL, 6 | email text NOT NULL, 7 | is_used boolean NOT NULL DEFAULT false, 8 | create_time timestamptz NOT NULL DEFAULT NOW() 9 | ); 10 | 11 | CREATE INDEX claim_title_tokens_token_index ON claim_title_tokens(token); 12 | 13 | ALTER TABLE signup_tokens 14 | DROP COLUMN gender, 15 | DROP COLUMN birth_year, 16 | DROP COLUMN license_id; -------------------------------------------------------------------------------- /api-service/migrations/20230226161606_robots.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | ADD COLUMN api_key_hashed bytea DEFAULT NULL, 3 | ADD COLUMN api_key_tail varchar(5) DEFAULT NULL, 4 | ADD COLUMN is_robot boolean NOT NULL DEFAULT FALSE; 5 | 6 | ALTER TYPE notification_kind ADD VALUE 'mention'; 7 | 8 | CREATE TABLE webhooks ( 9 | id bigserial PRIMARY KEY, 10 | user_id bigint REFERENCES users (id) NOT NULL, 11 | target_url text NOT NULL, 12 | secret varchar(32) NOT NULL DEFAULT '', 13 | create_time timestamptz NOT NULL DEFAULT NOW() 14 | -- event_set notification_kind, 15 | ); 16 | -------------------------------------------------------------------------------- /api-service/migrations/20220207115827_simplify_force.sql.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE articles 2 | DROP COLUMN category_id, 3 | ADD fields TEXT NOT NULL, 4 | ADD category TEXT NOT NULL; 5 | 6 | ALTER TABLE drafts 7 | DROP COLUMN category_id, 8 | ADD bonds TEXT NOT NULL, 9 | ADD category TEXT; 10 | 11 | ALTER TABLE article_bond_fields 12 | RENAME TO article_bonds; 13 | 14 | DROP TABLE categories; 15 | 16 | ALTER TABLE article_bonds 17 | RENAME COLUMN article_id TO from_id; 18 | 19 | ALTER TABLE article_bonds 20 | RENAME COLUMN value TO to_id; 21 | 22 | ALTER TABLE article_bonds 23 | DROP COLUMN name; -------------------------------------------------------------------------------- /api-service/src/util/token.rs: -------------------------------------------------------------------------------- 1 | use crate::custom_error::Fallible; 2 | use rand::{distributions::Alphanumeric, Rng}; 3 | 4 | pub fn generate_token() -> String { 5 | rand::thread_rng() 6 | .sample_iter(&Alphanumeric) 7 | .take(32) 8 | .collect::() 9 | } 10 | 11 | // 回傳 (salt, hash) 12 | pub fn generate_password_hash(password: &str) -> Fallible<(Vec, Vec)> { 13 | let salt = rand::thread_rng().gen::<[u8; 16]>(); 14 | let hash = argon2::hash_raw(password.as_bytes(), &salt, &argon2::Config::default())?; 15 | Ok((salt.to_vec(), hash)) 16 | } 17 | -------------------------------------------------------------------------------- /frontend/app/web/src/css/party/party_detail.module.css: -------------------------------------------------------------------------------- 1 | .partyDetail { 2 | margin-top: 20px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | & .partyName { 7 | font-size: 24px; 8 | color: var(--font-color); 9 | } 10 | & .boardName { 11 | font-size: 16px; 12 | color: var(--light-font-color); 13 | margin-left: 20px; 14 | } 15 | & .createBoardBlock { 16 | width: 600px; 17 | & .createButton { 18 | cursor: pointer; 19 | } 20 | margin-top: 20px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /api-service/migrations/20220213154105_add_title_and_certification_table.sql: -------------------------------------------------------------------------------- 1 | -- 使用者擁有的身份及認證的信箱 2 | 3 | CREATE TABLE title_authentication_user ( 4 | id bigserial PRIMARY KEY, 5 | user_id bigint REFERENCES users (id) NOT NULL, 6 | title text NOT NULL DEFAULT '', 7 | UNIQUE (user_id, title) 8 | ); 9 | 10 | CREATE INDEX title_authentication_user_index ON title_authentication_user (user_id); 11 | 12 | CREATE TABLE title_authentication_unique_id ( 13 | id bigserial PRIMARY KEY, 14 | title text NOT NULL DEFAULT '', 15 | unique_id text NOT NULL DEFAULT '', 16 | UNIQUE (title, unique_id) 17 | ); 18 | -------------------------------------------------------------------------------- /api-service/src/redis/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | use crate::custom_error::Fallible; 3 | use redis::{aio::Connection, Client}; 4 | use state::Storage; 5 | 6 | static CLIENT: Storage = Storage::new(); 7 | 8 | pub async fn init() -> Fallible<()> { 9 | let conf = config::get_config(); 10 | let client = Client::open(&*conf.redis.host)?; 11 | assert!(CLIENT.set(client), "Redis 客戶端被重複創建"); 12 | Ok(()) 13 | } 14 | pub async fn get_conn() -> Fallible { 15 | let conn = CLIENT.get().get_async_connection().await?; 16 | Ok(conn) 17 | } 18 | 19 | pub mod hot_articles; 20 | -------------------------------------------------------------------------------- /api-service/migrations/20211123121445_introduce_private_relations.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_relations 2 | ADD is_public boolean NOT NULL 3 | DEFAULT (false); 4 | 5 | UPDATE user_relations 6 | SET kind = 'follow' WHERE kind = 'openly_follow'; 7 | 8 | UPDATE user_relations 9 | SET kind = 'hate' WHERE kind = 'openly_hate'; 10 | 11 | -- In this migration we just deprecate 'openly_follow' and 'openly_hate' while keeping them in ENUM user_relation_kind. 12 | -- Or we could create new ENUM and drop the old one if needed. 13 | -- https://stackoverflow.com/questions/25811017/how-to-delete-an-enum-type-value-in-postgres 14 | -------------------------------------------------------------------------------- /api-service/src/service/hot_articles.rs: -------------------------------------------------------------------------------- 1 | use crate::custom_error::Fallible; 2 | use crate::redis::hot_articles; 3 | 4 | pub async fn init() -> Fallible { 5 | hot_articles::init().await 6 | } 7 | 8 | pub async fn update_article_score(article_id: i64) -> Fallible { 9 | hot_articles::update_article_score(article_id).await 10 | } 11 | pub async fn set_hot_article_score_first_time(article_id: i64) -> Fallible { 12 | hot_articles::set_hot_article_score_first_time(article_id).await 13 | } 14 | pub async fn get_hot_articles() -> Fallible> { 15 | hot_articles::get_hot_articles().await 16 | } 17 | -------------------------------------------------------------------------------- /frontend/app/web/src/tsx/components/number_over.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import style from '../../css/components/number_over.module.css'; 3 | 4 | export function NumberOver(props: { 5 | children: React.ReactNode, 6 | number: number, 7 | className?: string, 8 | top?: string, 9 | left?: string 10 | }): JSX.Element { 11 | return
12 | {props.children} 13 | { 14 | props.number > 0 ? 15 | 16 | {props.number} 17 | : 18 | <> 19 | } 20 |
; 21 | } -------------------------------------------------------------------------------- /frontend/app/web/src/css/header/search_bar.module.css: -------------------------------------------------------------------------------- 1 | .searchPart { 2 | line-height: 40px; 3 | cursor: text; 4 | flex: 1; 5 | padding-left: 20px; 6 | padding-right: 20px; 7 | 8 | display: flex; 9 | flex-direction: row; 10 | justify-content: center; 11 | 12 | & select { 13 | margin: 0; 14 | width: 100px; 15 | } 16 | & input { 17 | margin: 0; 18 | padding: 10px; 19 | flex: 1; 20 | max-width: 700px; 21 | 22 | border-radius: 3px; 23 | border: 1px solid var(--border-color); 24 | background: var(--background-color); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs-website/docs/早期文件/docker啓動.md: -------------------------------------------------------------------------------- 1 | # docker 啓動 2 | 3 | 1. 設置 `carbonbond.release.toml` 設定檔 4 | 5 | ```sh 6 | cd api-service/config # 依循第 2 步 docker-compose 啓動後,會讀取 config/ 下的設定檔 7 | cp carbonbond.docker.toml carbonbond.release.toml 8 | ``` 9 | 10 | 2. 修改 deply/docker-compose.yml 11 | 12 | (若您僅要執行 docker hub 上最新的映像檔,可省略本步驟) 13 | 14 | 根據該檔案中的說明,註解/反註解特定程式碼 15 | 16 | 3. 根據目前程式碼製作 docker 映像檔 17 | 18 | (若您僅要執行 docker hub 上最新的映像檔,可省略本步驟) 19 | 20 | ```sh 21 | cd deploy 22 | sh build.sh 23 | docker compose build 24 | ``` 25 | 26 | 4. 啓動 docker 27 | 28 | ```sh 29 | cd deploy 30 | docker compose up 31 | ``` 32 | 33 | 需先關閉本地端的 postgresql ,否則埠口會撞到 -------------------------------------------------------------------------------- /frontend/app/web/src/css/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' { 2 | const classes: { [key: string]: string }; 3 | export default classes; 4 | } 5 | 6 | declare module '*.png' { 7 | const value: string; 8 | export default value; 9 | } 10 | 11 | declare module '*.svg' { 12 | const value: string; 13 | export default value; 14 | } 15 | 16 | declare module '*.mp3' { 17 | const value: string; 18 | export default value; 19 | } 20 | 21 | declare module '*.md' { 22 | const attributes: Record; 23 | 24 | import React from 'react'; 25 | const ReactComponent: React.VFC; 26 | 27 | export { attributes, ReactComponent }; 28 | } -------------------------------------------------------------------------------- /frontend/app/web/src/tsx/bottom_panel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import style from '../css/bottom_panel/bottom_panel.module.css'; 3 | import { BottomPanelState } from './global_state/bottom_panel'; 4 | import { EditorPanel } from './editor_panel'; 5 | import { DesktopChatRoomPanel } from './chatroom_panel'; 6 | 7 | 8 | function BottomPanel(): JSX.Element { 9 | const { chatrooms } = BottomPanelState.useContainer(); 10 | return
11 | {chatrooms.map(room =>
)} 12 | 13 |
; 14 | } 15 | 16 | export { 17 | BottomPanel 18 | }; -------------------------------------------------------------------------------- /frontend/app/web/src/css/avatar.module.css: -------------------------------------------------------------------------------- 1 | .avatar { 2 | & img { 3 | max-height: 100%; 4 | max-width: 100%; 5 | } 6 | & .editPrompt { 7 | color: white; 8 | font-size: 14px; 9 | background: black; 10 | border-radius: 5px; 11 | padding: 5px; 12 | position: absolute; 13 | left: 5px; 14 | bottom: 5px; 15 | } 16 | } 17 | .avatar.isMine { 18 | & * { 19 | cursor: pointer; 20 | } 21 | & input { 22 | display: none; 23 | } 24 | } 25 | .cropper { 26 | & img { 27 | max-width: 300px; 28 | max-height: 300px; 29 | } 30 | } 31 | .buttonSet { 32 | margin-top: 10px; 33 | & button { 34 | margin-right: 5px; 35 | } 36 | } -------------------------------------------------------------------------------- /docs-website/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | title: 碳鍵 5 | titleTemplate: 次世代筆戰平台 6 | 7 | hero: 8 | name: 碳鍵 9 | text: 次世代筆戰平台 10 | tagline: 圖像化掌握全盤戰局 11 | image: 12 | src: /icon.svg 13 | alt: 碳鍵圖標 14 | width: 192 15 | actions: 16 | - theme: brand 17 | text: 出發 18 | link: /基礎/碳鍵是什麼 19 | - theme: alt 20 | text: 源碼 21 | link: https://github.com/carbon-bond/carbonbond 22 | features: 23 | - title: 鍵結 24 | details: 以鍵結表現文章之間的關係(激賞、厭惡、提及……) 25 | - title: 文章結構 26 | details: 板主可設定看板的分類,使得分類的文章必須符合指定格式 27 | - title: 匿名 28 | details: 留言與發表文章時,可以選擇是否匿名 29 | - title: 聊天室 30 | details: 與有興趣板友的發起即時通訊 31 | --- 32 | -------------------------------------------------------------------------------- /frontend/script/inject/data/crouching_dragon.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "name": "臥龍專板", 3 | "force": [ 4 | "PTT文章 {", 5 | "單行 看板", 6 | "單行 作者", 7 | "單行 時間", 8 | "單行 分類", 9 | "文本 內文", 10 | "單行 網址", 11 | "}", 12 | "PTT回文 {", 13 | "單行 看板", 14 | "單行 作者", 15 | "單行 時間", 16 | "鍵結[PTT文章] 原文[0~1]", 17 | "文本/.{1,}/ 內文", 18 | "單行 網址", 19 | "單行 原文網址", 20 | "}", 21 | "PTT推文 @[衛星] {", 22 | "鍵結[PTT文章,PTT回文] 原文", 23 | "文本/.{1,}/ 內文", 24 | "單行 作者", 25 | "單行 時間", 26 | "}", 27 | "回文 {", 28 | "鍵結[*] 原文[0~1]", 29 | "文本/.{1,}/ 內文", 30 | "}", 31 | "留文 @[衛星] {", 32 | "鍵結[*] 原文", 33 | "文本/.{1,}/ 內文", 34 | "}" 35 | ], 36 | "articles": [] 37 | }] 38 | -------------------------------------------------------------------------------- /frontend/app/web/src/css/signup_page.module.css: -------------------------------------------------------------------------------- 1 | .signupPage { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: center; 5 | & .signupForm { 6 | width: 40%; 7 | margin-top: 8%; 8 | & .counter { 9 | margin-bottom: 20px; 10 | } 11 | & .username { 12 | width: 100%; 13 | margin-bottom: 10px; 14 | } 15 | & .password { 16 | width: 100%; 17 | margin-bottom: 10px; 18 | } 19 | & button { 20 | margin-top: 12px; 21 | } 22 | & .terms { 23 | font-size: 14px; 24 | margin-top: 2px; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /api-service/migrations/20201108205511_notification.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE notification_kind AS ENUM ( 2 | 'follow', 3 | 'hate', 4 | 'article_replied', 5 | 'article_bad_replied', 6 | 'article_good_replied' 7 | ); 8 | 9 | CREATE TABLE notifications ( 10 | id bigserial PRIMARY KEY, 11 | user_id bigint REFERENCES users (id) NOT NULL, 12 | user2_id bigint REFERENCES users (id), 13 | board_id bigint REFERENCES boards (id), 14 | article1_id bigint REFERENCES articles (id), 15 | article2_id bigint REFERENCES articles (id), 16 | kind notification_kind NOT NULL, 17 | quality bool, -- NULL 表中性,true 表捷報,false 表惡耗 18 | read bool NOT NULL DEFAULT FALSE, 19 | create_time timestamptz NOT NULL DEFAULT NOW() 20 | ); 21 | 22 | -------------------------------------------------------------------------------- /docs-website/docs/進階/HTTPS.md: -------------------------------------------------------------------------------- 1 | # HTTPS 2 | 3 | `frontend` 容器中的 Nginx 預設使用 HTTP ,然而 HTTP 傳輸明文,這導致許多安全隱患,例如,在公共網路中使用密碼登入時,若有惡意人士竊聽,將有洩漏密碼的危險。是故,正式上線的網站都應採用 HTTPS 來保證安全與隱私。 4 | 5 | 本文提供兩種方式為碳鍵加上 HTTPS。 6 | 7 | ## 方法一: 設定 Nginx 8 | 9 | 1. 申請 SSL 憑證,你可以在 [Let's Encrypt](https://letsencrypt.org/) 免費申請,或是其他供應商申請,但他們可能會收取費用。 10 | 2. 按照 [Nginx 官方文件](http://nginx.org/en/docs/http/configuring_https_servers.html)修改 [Nginx 設定檔](https://github.com/carbon-bond/carbonbond/blob/master/frontend/app/web/deploy/nginx.conf),為其加入 SSL 憑證的設定。 11 | 3. 在本地端重新生成 `frontend` 的 docker 映像檔。(TODO:補充具體步驟) 12 | 13 | ## 方法二: Caddy 反向代理 14 | 15 | Caddy 是一個 HTTPS 的伺服器軟體,無需任何設定,開箱即能使用 HTTPS。您可以在 443 埠口架設 Caddy ,再將所有請求反向代理給 `frontend`。這會是最容易的方法,但注意再增加一層反向代理是必帶來更多性能損耗。 16 | -------------------------------------------------------------------------------- /api-service/src/api/model/webhook.rs: -------------------------------------------------------------------------------- 1 | // 本檔案存放碳鍵發送給機器人的 webhook API 定義 2 | 3 | use chitin::*; 4 | #[chitin_model] 5 | pub mod webhook_model_root { 6 | use chitin::*; 7 | use serde::{Deserialize, Serialize}; 8 | use typescript_definitions::{TypeScriptify, TypeScriptifyTrait}; 9 | 10 | #[derive(Serialize, Deserialize, TypeScriptify, Clone, Debug)] 11 | pub struct MentionInComment { 12 | pub article_id: i64, 13 | pub comment_id: i64, 14 | pub author_id: i64, 15 | pub comment_content: String, 16 | } 17 | 18 | #[derive(Serialize, Deserialize, TypeScriptify, Clone, Debug)] 19 | pub enum API { 20 | MentionedInComment(MentionInComment), 21 | } 22 | } 23 | 24 | pub use webhook_model_root::*; 25 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | paths: 7 | - 'frontend/**' 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | working-directory: ./frontend 13 | 14 | jobs: 15 | frontend-build: 16 | runs-on: ubuntu-20.04 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-node@v1 20 | with: 21 | node-version: '14.x' 22 | - uses: c-hive/gha-yarn-cache@v2 23 | with: 24 | directory: ./frontend 25 | - run: yarn install 26 | - run: yarn lint 27 | - run: yarn workspace web build 28 | - run: yarn workspace web check-ts 29 | - run: yarn workspace web check-css 30 | - run: yarn workspace web test 31 | 32 | -------------------------------------------------------------------------------- /frontend/app/web/src/css/notification.module.css: -------------------------------------------------------------------------------- 1 | .tag { 2 | color: var(--sub-theme-color) 3 | } 4 | .notificationSeparater { 5 | border: none; 6 | border-top: 1px solid var(--gray-2); 7 | width: 80%; 8 | margin: 0 auto; 9 | } 10 | .notificationList { 11 | overflow: scroll; 12 | & .notificationRow { 13 | font-size: 14px; 14 | & .notificationSpace { 15 | min-width: 20px; 16 | } 17 | } 18 | } 19 | .notificationMessage { 20 | margin: 10px 0; 21 | flex: 1; 22 | } 23 | .time { 24 | color: var(--gray-3); 25 | } 26 | .row:hover { 27 | background-color: var(--hover-background-color); 28 | } 29 | .row { 30 | padding: 5px 0px; 31 | cursor: pointer; 32 | display: flex; 33 | flex-wrap: wrap; 34 | } -------------------------------------------------------------------------------- /frontend/app/web/src/css/board/graph_view.module.css: -------------------------------------------------------------------------------- 1 | .articleBlock { 2 | position: absolute; 3 | transition: .2s; 4 | } 5 | .wrapper { 6 | width: 100%; 7 | position: relative; 8 | height: 100vh; 9 | 10 | & .panel { 11 | opacity: 0.8; 12 | padding: 10px; 13 | z-index: 9; 14 | background-color: white; 15 | box-shadow: 0px 0px 2px 0.5px rgba(0, 0, 0, 0.2); 16 | position: absolute; 17 | top: 5px; 18 | right: 5px; 19 | color: gray; 20 | 21 | & hr { 22 | border-color: var(--gray-1); 23 | } 24 | } 25 | } 26 | .svgBlock { 27 | width: 100%; 28 | position: relative; 29 | background-color: var(--gray-1); 30 | & svg:active { 31 | cursor: move; 32 | } 33 | } -------------------------------------------------------------------------------- /docs-website/docs/早期文件/rust設置.md: -------------------------------------------------------------------------------- 1 | # rust 設置 2 | 3 | ## 版本 4 | 5 | 1.56.0-nightly 以上 6 | 7 | ## 安裝 8 | 9 | 參考 [官方網站](https://www.rust-lang.org/tools/install)。 10 | 11 | 將工具鏈版本設置為 nightly。 12 | ``` sh 13 | rustup default nightly 14 | rustup update 15 | ``` 16 | 17 | ## rustfmt 18 | 19 | 欲將將整個專案的 rust 檔案都標準格式化,執行 20 | ``` sh 21 | cargo fmt 22 | ``` 23 | 24 | 若只想檢查語法但不想修改檔案,可以執行 25 | ``` sh 26 | cargo fmt -- --check 27 | ``` 28 | 29 | 可修改專案底下的 .rustfmt.toml 檔案來設定格式,詳見[官方文件](https://github.com/rust-lang/rustfmt/blob/master/Configurations.md) 30 | 31 | ## vscode 建議設定 32 | 33 | 安裝 [rust-analyzer 外掛](https://marketplace.visualstudio.com/items?itemName=matklad.rust-analyzer) 34 | 35 | ### 存檔時格式化程式碼 36 | 在 vscode 的 setting.json 設定 37 | ``` 38 | "[rust]":{ 39 | "editor.formatOnSave": true, 40 | } 41 | ``` -------------------------------------------------------------------------------- /frontend/app/web/vite_plugins/mobile.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, ViteDevServer } from 'vite'; 2 | 3 | function isMobile(user_agent: string): boolean { 4 | if (/iPad/i.test(user_agent)) { 5 | return false; 6 | } else { 7 | return /Mobile/i.test(user_agent); 8 | } 9 | } 10 | 11 | const mobilePlugin = (): Plugin => ({ 12 | name: 'mobilePlugin', 13 | configureServer(server: ViteDevServer) { 14 | server.middlewares.use('/', (req, _, next) => { 15 | if (isMobile(req.headers['user-agent'] ?? '')) { 16 | if (req.url == '/' || req.url?.startsWith('/app')) { 17 | req.url = '/index.mobile.html'; 18 | } 19 | } else { 20 | if (req.url == '/' || req.url?.startsWith('/app')) { 21 | req.url = '/index.html'; 22 | } 23 | } 24 | next(); 25 | }); 26 | } 27 | }); 28 | 29 | export default mobilePlugin; -------------------------------------------------------------------------------- /frontend/app/web/src/css/components/drop_down.module.css: -------------------------------------------------------------------------------- 1 | .wrap { 2 | position: relative; 3 | height: 100%; 4 | & .button { 5 | height: 100%; 6 | cursor: pointer; 7 | } 8 | & .dropDown { 9 | position: absolute; 10 | left: 0px; 11 | right: 0px; 12 | display: flex; 13 | flex-direction: column; 14 | align-content: center; 15 | z-index: 100; 16 | & .triangle { 17 | align-self: center; 18 | width: 0; 19 | height: 0; 20 | border-left: 15px solid transparent; 21 | border-right: 15px solid transparent; 22 | border-bottom: 15px solid var(--third-theme-color); 23 | } 24 | & .body { 25 | align-self: center; 26 | position: relative; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /api-service/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate derive_more; 3 | 4 | pub mod config; 5 | pub mod custom_error; 6 | pub mod force; 7 | 8 | #[cfg(not(feature = "prepare"))] 9 | pub mod api; 10 | #[cfg(not(feature = "prepare"))] 11 | pub mod chat; 12 | #[cfg(not(feature = "prepare"))] 13 | pub mod db; 14 | #[cfg(not(feature = "prepare"))] 15 | pub mod email; 16 | #[cfg(not(feature = "prepare"))] 17 | pub mod notification; 18 | #[cfg(not(feature = "prepare"))] 19 | pub mod redis; 20 | #[cfg(not(feature = "prepare"))] 21 | pub mod routes; 22 | #[cfg(not(feature = "prepare"))] 23 | pub mod service; 24 | #[cfg(not(feature = "prepare"))] 25 | pub mod service_manager; 26 | #[cfg(not(feature = "prepare"))] 27 | pub mod util; 28 | 29 | #[cfg(not(feature = "prepare"))] 30 | pub mod context; 31 | 32 | #[cfg(not(feature = "prepare"))] 33 | use context::{Context, Ctx}; 34 | -------------------------------------------------------------------------------- /frontend/app/web/src/tsx/components/scalable_input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function ScalableInput(props: {onChange: ((value: string) => void), value: string}): JSX.Element { 4 | const inputRef = React.useRef(null); 5 | const measurer = React.useRef(null); 6 | const [width, setWidth] = React.useState(30); 7 | 8 | React.useEffect(() => { 9 | if (measurer.current) { 10 | setWidth(measurer.current.offsetWidth + 10); 11 | } 12 | }, [props.value]); 13 | 14 | return 15 | { 16 | <> 17 | { 22 | props.onChange(evt.target.value); 23 | }} 24 | /> 25 | 26 | } 27 | {props.value} 28 | ; 29 | } -------------------------------------------------------------------------------- /api-service/config/carbonbond.docker.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | address = "0.0.0.0" 3 | port = 8080 4 | base_url = "http://localhost" 5 | 6 | [email] 7 | domain = "mail.my-domain.com" 8 | from = "碳鍵 " 9 | # fake_receiver = "fake-receiver@email" 10 | signup_whitelist = [] 11 | [email.driver] 12 | type = "Log" 13 | 14 | [database] 15 | # url 的格式為 "postgres://[用戶名]:[密碼]@[資料庫位址]:[埠口]/[資料庫名]" 16 | # url = "postgres://postgres:mypassword@postgres:5432/carbonbond" 17 | dbname = "carbonbond" 18 | username = "postgres" 19 | password = "mypassword" 20 | port = 5432 21 | host = "postgres" 22 | data_path = "data" 23 | max_conn = 10 24 | 25 | [redis] 26 | host = "redis://redis/" 27 | 28 | [account] 29 | allow_self_signup = true 30 | allow_invitation_signup = true 31 | session_expire_seconds = 604800 32 | min_password_length = 6 33 | max_password_length = 100 34 | 35 | [business] 36 | advertisement_contact_mail = "business@my-domain.com" -------------------------------------------------------------------------------- /frontend/app/web/src/css/markdown.css: -------------------------------------------------------------------------------- 1 | .markdown { 2 | & li { 3 | margin: 2px 0px; 4 | } 5 | & ul { 6 | padding-left: 20px; 7 | } 8 | & ol { 9 | padding-left: 20px; 10 | } 11 | & p { 12 | margin: 15px 0px; 13 | } 14 | & h2 { 15 | margin: 15px 0px; 16 | } 17 | & h3 { 18 | margin: 8px 0px; 19 | } 20 | & blockquote { 21 | background: var(--gray-1); 22 | border-left: 4px solid var(--gray-3); 23 | margin: 8px 5px; 24 | padding: 5px 10px; 25 | } 26 | & pre > code { 27 | width: 100%; 28 | color: var(--gray-2); 29 | display: inline-block; 30 | overflow: scroll; 31 | overflow-y: hidden; 32 | background-color: black; 33 | border-radius: 10px; 34 | padding: 10px; 35 | margin: 10px 0px; 36 | } 37 | & img { 38 | max-width: 100%; 39 | } 40 | } -------------------------------------------------------------------------------- /frontend/app/web/src/css/components/select.module.css: -------------------------------------------------------------------------------- 1 | .select { 2 | position: relative; 3 | cursor: pointer; 4 | background-color: white; 5 | border-style: solid; 6 | border-radius: 3px; 7 | border-width: 1px; 8 | border-color: gray; 9 | & .btn { 10 | height: 100%; 11 | width: 100%; 12 | display: flex; 13 | align-items: center; 14 | flex-direction: row; 15 | } 16 | & .background { 17 | position: absolute; 18 | left: -0.5px; 19 | width: 100%; 20 | overflow-y: auto; 21 | box-shadow: 2px 3px 1px #ddd; 22 | background-color: white; 23 | border-style: solid; 24 | border-color: gray; 25 | border-width: 1; 26 | transition: .2s; 27 | } 28 | & .option { 29 | height: 30px; 30 | width: 100%; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | } 35 | & .option:hover { 36 | background-color: var(--hover-color); 37 | } 38 | } -------------------------------------------------------------------------------- /frontend/app/web/src/ts/regex_util.ts: -------------------------------------------------------------------------------- 1 | // NOTE: 由於 emoji 每年都會增加,本函式可能需要更新 2 | export function isEmojis(s: string): boolean { 3 | const ranges = [ 4 | '\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]', 5 | ' ', // Also allow spaces 6 | ].join('|'); 7 | 8 | const removeEmoji = (str: string): string => str.replace(new RegExp(ranges, 'g'), ''); 9 | return !removeEmoji(s).length; 10 | } 11 | 12 | export const EMAIL_REGEX = new RegExp('.+@.+'); 13 | 14 | export function isEmail(s: string): boolean { 15 | return !!EMAIL_REGEX.test(s); 16 | } 17 | 18 | export function isLink(s: string): boolean { 19 | const pattern = new RegExp('^https?:\\/\\/'); 20 | return !!pattern.test(s); 21 | } 22 | 23 | export function isImageLink(s: string): boolean { 24 | return isLink(s) && (s.match(/\.(jpeg|jpg|gif|png)$/) != null); 25 | } 26 | 27 | export function isInteger(s: string): boolean { 28 | return /^-?\d+$/.test(s); 29 | } -------------------------------------------------------------------------------- /frontend/app/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 碳鍵 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/app/web/src/tsx/global_state/config.tsx: -------------------------------------------------------------------------------- 1 | import { API_FETCHER, unwrap } from 'carbonbond-api/api_utils'; 2 | import * as React from 'react'; 3 | import { createContainer } from 'unstated-next'; 4 | import { toastErr } from '../utils'; 5 | import { Config } from 'carbonbond-api/api_trait'; 6 | const { useState } = React; 7 | 8 | function useConfigState(): { server_config: Config } { 9 | const [server_config, setServerConfig] = useState({ 10 | min_password_length: 0, 11 | max_password_length: 10000, 12 | advertisement_contact_email: null 13 | }); 14 | 15 | async function getConfig(): Promise { 16 | try { 17 | const config = unwrap(await API_FETCHER.configQuery.queryConfig()); 18 | setServerConfig(config); 19 | } catch (err) { 20 | toastErr(err); 21 | } 22 | return; 23 | } 24 | 25 | React.useEffect(() => { 26 | getConfig(); 27 | }, []); 28 | 29 | return { server_config }; 30 | } 31 | 32 | export const ConfigState = createContainer(useConfigState); -------------------------------------------------------------------------------- /frontend/app/web/index.mobile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 碳鍵 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/app/web/src/ts/utils.ts: -------------------------------------------------------------------------------- 1 | function fallbackCopyToClipboard(text_to_copy: string): Promise { 2 | console.warn('瀏覽器剪貼簿僅在 https 或 localhost 可用,現採用兼容方式複製'); 3 | // 创建text area 4 | let text_area = document.createElement('textarea'); 5 | text_area.value = text_to_copy; 6 | // 使text area不在viewport,同时设置不可见 7 | text_area.style.position = 'fixed'; 8 | text_area.style.opacity = '0'; 9 | text_area.style.left = '-999999px'; 10 | text_area.style.top = '-999999px'; 11 | document.body.appendChild(text_area); 12 | text_area.focus(); 13 | text_area.select(); 14 | return new Promise((res, rej) => { 15 | // 执行复制命令并移除文本框 16 | document.execCommand('copy') ? res() : rej(); 17 | text_area.remove(); 18 | }); 19 | } 20 | 21 | export function copyToClipboard(text_to_copy: string): Promise { 22 | // navigator clipboard 需要https等安全上下文 23 | if (navigator.clipboard) { 24 | return navigator.clipboard.writeText(text_to_copy); 25 | } else { 26 | return fallbackCopyToClipboard(text_to_copy); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/app/web/src/css/setting_page.module.css: -------------------------------------------------------------------------------- 1 | .settingPage { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: center; 5 | padding-top: 5%; 6 | & .settings { 7 | width: 100%; 8 | max-width: 400px; 9 | & .setting { 10 | & .name { 11 | font-size: 18px; 12 | font-weight: bold; 13 | margin-bottom: 6px; 14 | } 15 | } 16 | & hr { 17 | border: 1px solid var(--gray-2); 18 | } 19 | } 20 | & .warning { 21 | margin-top: 12px; 22 | color: var(--red); 23 | } 24 | } 25 | 26 | .webhookPage { 27 | & .label { 28 | display: inline-block; 29 | width: 120px; 30 | } 31 | & .webhook { 32 | padding: 10px; 33 | margin-bottom: 30px; 34 | border: 1px solid var(--gray-3); 35 | flex-direction: row; 36 | display: flex; 37 | justify-content: space-between; 38 | align-items: center; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /api-service/src/email/mailgun.rs: -------------------------------------------------------------------------------- 1 | use crate::custom_error::{Error, Fallible}; 2 | use reqwest; 3 | use std::collections::HashMap; 4 | 5 | pub async fn send_via_mailgun( 6 | sender: &str, 7 | domain: &str, 8 | receiver: &str, 9 | subject: &str, 10 | html_content: &str, 11 | mailgun_api_key: &str, 12 | ) -> Fallible<()> { 13 | let mut form = HashMap::new(); 14 | form.insert("from", sender); 15 | form.insert("to", receiver); 16 | form.insert("subject", subject); 17 | form.insert("html", html_content); 18 | let url = format!( 19 | "https://api:{}@api.mailgun.net/v3/{}/messages", 20 | mailgun_api_key, domain 21 | ); 22 | let response = reqwest::Client::new().post(url).form(&form).send().await?; 23 | let is_success = response.status().is_success(); 24 | 25 | let ret_msg = &response.text().await?; 26 | log::debug!("mailgun 寄信回傳訊息:{}", ret_msg); 27 | 28 | if is_success { 29 | Ok(()) 30 | } else { 31 | Err(Error::new_internal(ret_msg)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/script/inject/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "lib": [ 6 | "es2015", 7 | "es2017", 8 | "esnext", 9 | "dom" 10 | ], 11 | "noEmit": true, /* Do not emit outputs. */ 12 | "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 13 | "strict": true, /* Enable all strict type-checking options. */ 14 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 15 | }, 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /frontend/app/web/src/css/components/modal_window.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | background-color: var(--sub-theme-color); 3 | height: 30px; 4 | display: flex; 5 | flex-direction: row; 6 | align-items: center; 7 | justify-content: space-between; 8 | & .leftSet { 9 | font-size: 18px; 10 | margin-left: 4px; 11 | } 12 | & .middleSet { 13 | flex-grow: 1; 14 | height: 100%; 15 | justify-content: center; 16 | } 17 | & .rightSet { 18 | font-size: 22px; 19 | margin-right: 4px; 20 | & .button { 21 | cursor: pointer; 22 | width: 25px; 23 | } 24 | } 25 | } 26 | .escape { 27 | cursor: pointer; 28 | font-size: 28px; 29 | position: absolute; 30 | top: -14px; 31 | right: -14px; 32 | color: red; 33 | } 34 | .body { 35 | border: var(--border-color) 1px solid; 36 | background: var(--background-color); 37 | padding: 0px 10px 10px; 38 | } 39 | .buttonBar { 40 | & button { 41 | margin: 0px 10px 0px 0px; 42 | } 43 | } -------------------------------------------------------------------------------- /frontend/app/web/src/tsx/law_page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import style from '../css/law_page.module.css'; 4 | import '../css/markdown.css'; 5 | 6 | import { ReactComponent as TermsComponent } from '../md/law/服務條款.md'; 7 | import { ReactComponent as RulesComponent } from '../md/law/論壇守則.md'; 8 | import { ReactComponent as BoardComponent } from '../md/law/看板和活動政策.md'; 9 | import { ReactComponent as BrandComponent } from '../md/law/品牌使用準則.md'; 10 | 11 | import { 12 | Routes, 13 | Route, 14 | } from 'react-router-dom'; 15 | 16 | function LawPage(): JSX.Element { 17 | return
18 |
19 | 20 | } /> 21 | } /> 22 | } /> 23 | } /> 24 | 25 |
26 |
; 27 | } 28 | 29 | export { LawPage }; -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "carbonbond", 3 | "version": "0.1.0", 4 | "description": "碳鍵次世代論壇", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "lint": "eslint --cache --ignore-path .gitignore --ext .js --ext .ts --ext .tsx ." 10 | }, 11 | "workspaces": [ 12 | "app/**", 13 | "lib/**", 14 | "script/**" 15 | ], 16 | "resolutions": { 17 | "@types/react": "17", 18 | "@types/react-dom": "17" 19 | }, 20 | "dependencies": { 21 | "react": "17.0.2", 22 | "react-dom": "17.0.2" 23 | }, 24 | "devDependencies": { 25 | "@types/jest": "27", 26 | "@types/react": "17", 27 | "@types/react-dom": "17", 28 | "@typescript-eslint/eslint-plugin": "^5.20.0", 29 | "@typescript-eslint/parser": "^5.20.0", 30 | "eslint": "^8.17.0", 31 | "eslint-plugin-react": "^7.30.0", 32 | "eslint-plugin-react-hooks": "^4.5.0", 33 | "jest": "27", 34 | "jest-environment-jsdom": "27", 35 | "ts-jest": "27", 36 | "ts-node": "^10.8.1", 37 | "typescript": "^4.7.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs-website/docs/基礎/安裝.md: -------------------------------------------------------------------------------- 1 | # 安裝 2 | 3 | 碳鍵依賴多項軟體跟開發工具,從頭編譯、並架設 Nginx, Redis 等等軟體會花上不少時間。 4 | 5 | 所幸,碳鍵官方提供了 docker 映像檔,能夠簡化架設流程。您僅需要先在你的機器上安裝 Docker 就可以了。 6 | 7 | ## 安裝 Docker 8 | 9 | Linux 用戶可根據 Docker 官網的指示安裝 [Docker Engine](https://docs.docker.com/engine/install/),Mac 與 Windows 用戶則適用 [Docker Desktop](https://docs.docker.com/desktop/)。 10 | 11 | ## 啓動碳鍵 12 | 13 | 1. 下載 carbonbond 原始碼。 14 | ``` 15 | git clone https://github.com/carbon-bond/carbonbond 16 | cd carbonbond 17 | ``` 18 | 2. 建立 carbonbond.release.toml 文件,請先直接複製原始碼中提供的 carbonbond.docker.toml。 19 | ``` 20 | cp api-service/config/carbonbond.docker.toml api-service/config/carbonbond.release.toml 21 | ``` 22 | 23 | 3. 啓動。 24 | ``` 25 | cd deploy 26 | sudo docker compose up 27 | ``` 28 | `docker compose` 會自動下載並執行最新的碳鍵映像檔,若您在終端看到 29 | 30 | > 靜候於 http://0.0.0.0:8080 31 | 32 | 代表碳鍵 API 伺服器已經佈署成功,用瀏覽器打開 http://localhost [^1],若您看到了論壇界面,恭喜您已經邁出了重大的一步,但您還需要一些額外設定才能開放給社羣使用。請看下一章[電郵設定](./%E9%9B%BB%E9%83%B5%E8%A8%AD%E5%AE%9A.md)。 33 | 34 | [^1]: localhost 的 80 埠口由一個 Nginx 容器負責監聽,而`靜候於 http://0.0.0.0:8080`這段文字是碳鍵的 API 伺服器打印出來的。Nginx 收到 API 請求時,會把請求反向代理給 8080 埠口。 35 | -------------------------------------------------------------------------------- /frontend/app/web/src/tsx/profile/user_card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { UserMini } from 'carbonbond-api/api_trait'; 4 | import style from '../../css/user_card.module.css'; 5 | 6 | function UserCard(props: { user: UserMini }): JSX.Element { 7 | const url = `/app/user/${props.user.user_name}`; 8 | 9 | return ( 10 |
11 |
12 | 13 |
14 |
15 |
{props.user.user_name}
16 |
{ 17 | props.user.sentence.length == 0 ? 18 | 尚無一句話介紹 : 19 | props.user.sentence 20 | }
21 |
22 |
23 | ☘️ 鍵能 {props.user.energy} 24 |
25 |
26 |
27 | 28 |
29 | ); 30 | } 31 | 32 | export { 33 | UserCard 34 | }; -------------------------------------------------------------------------------- /docs-website/docs/早期文件/資料庫設置.md: -------------------------------------------------------------------------------- 1 | # 資料庫設置 2 | ### PostgreSQL 3 | 4 | ### 安裝 5 | 6 | 執行 `apt install postgresql` 安裝 postgres 資料庫,並執行 `apt install libpq-dev` 安裝 libpq ,往後編譯時才能鏈接到。 7 | 8 | ### 設定 9 | 10 | 1. 設定 postgres 用戶的密碼 11 | ```sh 12 | sudo -iu postgres # 轉換成 postgres 使用者。 13 | psql # 進入 postgresql 的客戶端 14 | > ALTER USER postgres PASSWORD 'mypassword'; # 設定 postgres 用戶的密碼 15 | ``` 16 | 17 | 2. 修改 carbonbond.toml 或 carbonbond.dev.toml \[database\] 的 password 欄位 18 | 19 | 3. 將 postgres 用戶的登入方式改為使用密碼 20 | ```sh 21 | sudo vim /etc/postgresql//main/pg_hba.conf 22 | # 將 23 | # local all postgres peer 24 | # 改為 25 | # local all postgres md5 26 | ``` 27 | 28 | 4. 啟動/重啟 postgresql 29 | ```sh 30 | sudo service postgresql restart 31 | ``` 32 | 33 | ### 資料庫遷移 34 | 35 | 第一次使用,執行以下指令以創建資料庫及表格 36 | 37 | ``` 38 | cargo run --bin prepare --features=prepare -- -m 39 | ``` 40 | 41 | 後續遷移資料庫時可執行 42 | ``` 43 | cargo run --bin prepare --features=prepare -- -cm 44 | ``` 45 | 46 | ## redis 47 | 48 | ``` 49 | sudo apt install redis 50 | sudo service redis-server start 51 | ``` -------------------------------------------------------------------------------- /frontend/app/web/src/tsx/components/drop_down.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import style from '../../css/components/drop_down.module.css'; 3 | 4 | // 點擊 Button 後,Body 將下拉顯示出來 5 | export function DropDown(props: { 6 | button: JSX.Element, 7 | body: null | JSX.Element, 8 | onExtended?: Function, 9 | forced_expanded?: boolean, 10 | hide_triangle?: boolean, 11 | }): JSX.Element { 12 | const [extended, setExtended] = React.useState(false); 13 | let should_expand = extended && props.body != null; 14 | if (typeof props.forced_expanded != 'undefined') { 15 | should_expand = props.forced_expanded; 16 | } 17 | return
18 |
{ 19 | setExtended(!extended); 20 | if (props.onExtended) { 21 | props.onExtended(); 22 | } 23 | }}> 24 | {props.button} 25 |
26 |
27 | { 28 | should_expand 29 | ? <> 30 | {props.hide_triangle ? null :
} 31 |
{props.body}
32 | 33 | : null 34 | } 35 |
36 |
; 37 | } -------------------------------------------------------------------------------- /frontend/app/web/src/css/layout.css: -------------------------------------------------------------------------------- 1 | .app { 2 | } 3 | .appMobile { 4 | & .other { 5 | padding-bottom: var(--footer-height); 6 | & .mainBody { 7 | overflow-y: hidden; 8 | } 9 | } 10 | } 11 | .header { 12 | position: fixed; 13 | z-index: 300; 14 | } 15 | .mobileFullContent { 16 | width: 100%; 17 | height: calc(100vh - var(--header-height) - var(--footer-height)); 18 | position: fixed; 19 | top: var(--header-height); 20 | } 21 | 22 | .content { 23 | width: 100%; 24 | display: flex; 25 | justify-content: center; 26 | } 27 | 28 | .other { 29 | background-color: var(--gray-1); 30 | top: var(--header-height); 31 | min-height: calc(100vh - var(--header-height)); 32 | display: flex; 33 | flex-direction: row; 34 | position: relative; 35 | & .mainBody { 36 | height: 100%; 37 | flex: 1; 38 | & .forumBody { 39 | width: 100%; 40 | height: 100%; 41 | } 42 | } 43 | } 44 | 45 | .mainContent { 46 | max-width: 600px; 47 | width: 100%; 48 | } 49 | .rightSideBar { 50 | padding: 10px 20px; 51 | width: 335px; 52 | } -------------------------------------------------------------------------------- /frontend/app/web/src/css/board_list.module.css: -------------------------------------------------------------------------------- 1 | .boardList { 2 | margin-top: 20px; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | & > div { 7 | width: 100%; 8 | background-color: var(--background-color); 9 | & .boardBlock:not(:first-child) { 10 | border-top: 0px; 11 | } 12 | & .boardBlock:hover { 13 | background: var(--hover-background-color) 14 | } 15 | & .boardBlock { 16 | border: 1px var(--border-color) solid; 17 | width: 100%; 18 | padding: 5px; 19 | & .info { 20 | display: flex; 21 | flex-direction: row; 22 | & .name { 23 | color: black; 24 | font-size: 16px; 25 | } 26 | & .type { 27 | flex: 1px; 28 | color: var(--green); 29 | margin-left: 5px; 30 | } 31 | } 32 | & .title { 33 | color: black; 34 | font-size: 12px; 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /frontend/app/web/deploy/nginx.conf: -------------------------------------------------------------------------------- 1 | map $http_user_agent $index { 2 | default /index.html; 3 | ~*iPad /index.html; 4 | ~*Mobile|Android|webOS|iPhone|iPod|BlackBerry /index.mobile.html; 5 | } 6 | server { 7 | listen 80; 8 | server_name 0.0.0.0; 9 | access_log /var/log/nginx/access_log; 10 | error_log /var/log/nginx/error_log; 11 | charset utf-8; 12 | gzip on; 13 | gzip_types text/plain text/css application/json application/javascript application/x-javascript text/javascript image/svg+xml image/png image/jpeg; 14 | gzip_min_length 1000; 15 | gzip_proxied any; 16 | 17 | location ~ ^/(api|avatar) { 18 | proxy_pass http://api-service:8080; 19 | } 20 | 21 | location /chat { 22 | proxy_pass http://api-service:8080; 23 | proxy_http_version 1.1; 24 | proxy_set_header Upgrade $http_upgrade; 25 | proxy_set_header Connection "Upgrade"; 26 | proxy_set_header Host $host; 27 | } 28 | 29 | location ~ \.(js|css)$ { 30 | root /usr/share/nginx/html; 31 | expires 365d; 32 | } 33 | 34 | location / { 35 | root /usr/share/nginx/html; 36 | try_files $uri /$uri $index; 37 | } 38 | } -------------------------------------------------------------------------------- /api-service/src/email/smtp.rs: -------------------------------------------------------------------------------- 1 | use crate::custom_error::{Error, Fallible}; 2 | use lettre::{ 3 | message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport, 4 | AsyncTransport, Message, Tokio1Executor, 5 | }; 6 | 7 | pub async fn send_via_smtp( 8 | sender: &str, 9 | receiver: &str, 10 | subject: &str, 11 | html_content: String, 12 | smtp_server: &str, 13 | smtp_username: &str, 14 | smtp_password: &str, 15 | ) -> Fallible<()> { 16 | let email = Message::builder() 17 | .from(sender.parse().unwrap()) 18 | .to(receiver.parse().unwrap()) 19 | .subject(subject) 20 | .header(ContentType::TEXT_HTML) 21 | .body(html_content) 22 | .unwrap(); 23 | 24 | let creds = Credentials::new(smtp_username.to_owned(), smtp_password.to_owned()); 25 | 26 | let mailer: AsyncSmtpTransport = 27 | AsyncSmtpTransport::::relay(smtp_server) 28 | .unwrap() 29 | .credentials(creds) 30 | .build(); 31 | 32 | match mailer.send(email).await { 33 | Ok(_) => Ok(()), 34 | Err(e) => Err(Error::new_internal(format!("{:?}", e))), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/app/web/src/tsx/mobile/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { 4 | BrowserRouter as Router, 5 | } from 'react-router-dom'; 6 | 7 | import 'react-toastify/dist/ReactToastify.css'; 8 | import 'normalize.css'; 9 | import '../../css/variable.css'; 10 | import '../../css/layout.css'; 11 | import '../../css/global.css'; 12 | 13 | import { Header } from './header'; 14 | import { Footer } from './footer'; 15 | import { MainRoutes } from '../app/main_routes'; 16 | import { init, useInit } from '../app/init'; 17 | import { Providers } from '../app/providers'; 18 | 19 | function MainBody(): JSX.Element { 20 | return
21 | 22 |
; 23 | } 24 | 25 | function Content(): JSX.Element { 26 | useInit(); 27 | return 28 |
29 |
30 | 31 |
32 |