├── .github └── workflows │ ├── build.yml │ ├── check.yml │ └── clean-cache.yml ├── .gitignore ├── .gitmodules ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── balance-updater ├── Cargo.toml └── src │ ├── cache.rs │ ├── lib.rs │ ├── pull.rs │ └── vaccum.rs ├── build-push.sh ├── cache-macro ├── Cargo.toml └── src │ └── lib.rs ├── cache ├── Cargo.toml └── src │ └── lib.rs ├── captcha ├── Cargo.toml └── src │ └── lib.rs ├── kite.toml ├── kite ├── Cargo.toml └── src │ ├── config.rs │ ├── db.rs │ ├── lib.rs │ ├── model.rs │ ├── model │ ├── badge.rs │ ├── balance.rs │ ├── board.rs │ ├── classroom_browser.rs │ ├── template.rs │ └── user.rs │ └── service.rs ├── kite3.service ├── loader ├── Cargo.toml └── src │ └── main.rs ├── rustfmt.toml ├── service-v2 ├── Cargo.toml └── src │ ├── captcha.rs │ ├── electricity.rs │ ├── error.rs │ ├── lib.rs │ └── response.rs └── service-v3 ├── Cargo.toml ├── build.rs └── src ├── authserver.rs ├── authserver ├── client.rs ├── portal.rs └── tls.rs ├── error.rs ├── lib.rs ├── model.rs ├── service.rs └── service ├── auth.rs ├── badge.rs ├── balance.rs ├── board.rs ├── captcha.rs ├── classroom_browser.rs ├── gen.rs ├── gen ├── badge.rs ├── balance.rs ├── board.rs ├── captcha.rs ├── classroom_browser.rs ├── exception.rs ├── freshman.rs ├── game.rs ├── ping.rs ├── template.rs ├── token.rs ├── typing.rs ├── user.rs └── yellow_page.rs ├── ping.rs ├── template.rs └── user.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | - name: Checkout submodules 18 | run: git submodule update --init --force --recursive --remote 19 | 20 | - name: Install latest stable 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | override: true 24 | toolchain: stable 25 | - name: Install Protoc 26 | uses: arduino/setup-protoc@v1 27 | 28 | # Restore cache 29 | - name: Restore cargo packages 30 | uses: actions/cache@v3 31 | env: 32 | cache-name: cache-cargo-registry 33 | with: 34 | path: "~/.cargo/" 35 | key: ${{ runner.os }}-build-${{ env.cache-name }} 36 | restore-keys: ${{ runner.os }}-build-${{ env.cache-name }} 37 | - name: Restore build product 38 | uses: actions/cache@v3 39 | env: 40 | cache-name: cache-build-product 41 | with: 42 | path: "./target/" 43 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./Cargo.lock') }} 44 | restore-keys: | 45 | ${{ runner.os }}-build-${{ env.cache-name }}- 46 | ${{ runner.os }}- 47 | 48 | # Build 49 | - name: Build 50 | uses: actions-rs/cargo@v1 51 | with: 52 | command: build 53 | args: --release --all-features 54 | 55 | push: 56 | needs: build 57 | runs-on: ubuntu-latest 58 | if: | 59 | github.ref == 'refs/heads/master' && 60 | github.repository == 'SIT-kite/kite-server' 61 | 62 | steps: 63 | - name: Restore build product 64 | uses: actions/cache@v3 65 | env: 66 | cache-name: cache-build-product 67 | with: 68 | path: "./target/" 69 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./Cargo.lock') }} 70 | restore-keys: | 71 | ${{ runner.os }}-build-${{ env.cache-name }}- 72 | ${{ runner.os }}- 73 | - name: Strip binary file 74 | run: | 75 | strip -s target/release/loader 76 | ls target/release -lh | grep 'loader$' | awk '{print "Binary size is", $5, "after strip" }' 77 | cp target/release/loader kite-server-v3 78 | 79 | - name: Copy file via SSH 80 | uses: appleboy/scp-action@master 81 | with: 82 | host: ${{ secrets.SERVER_HOST }} 83 | port: ${{ secrets.SERVER_PORT }} 84 | username: ${{ secrets.SERVER_USER }} 85 | password: ${{ secrets.SERVER_SECRET }} 86 | source: kite-server-v3 87 | target: ${{ secrets.DEPLOY_PATH }}/kite-server-v3.tmp 88 | - name: Deploy & Restart via SSH 89 | uses: appleboy/ssh-action@master 90 | with: 91 | host: ${{ secrets.SERVER_HOST }} 92 | port: ${{ secrets.SERVER_PORT }} 93 | username: ${{ secrets.SERVER_USER }} 94 | password: ${{ secrets.SERVER_SECRET }} 95 | # User home is the deployment path. 96 | script: | 97 | cp ./kite-server-v3 ./kite-server-v3.bak 98 | systemctl --user stop kite3.service 99 | mv ./kite-server-v3.tmp/kite-server-v3 kite-server-v3 100 | systemctl --user start kite3.service 101 | rm -rf ./kite-server-v3.tmp 102 | 103 | notification: 104 | runs-on: ubuntu-latest 105 | needs: [ push ] 106 | if: | 107 | github.event_name != 'pull_request' && 108 | github.repository == 'SIT-kite/kite-server' && 109 | always() 110 | steps: 111 | - uses: technote-space/workflow-conclusion-action@v2 112 | - uses: nelonoel/branch-name@v1.0.1 113 | - uses: benjlevesque/short-sha@v2.1 114 | id: short-sha 115 | with: { length: 7 } 116 | 117 | - name: 🤖 Build Result Notification 118 | uses: appleboy/telegram-action@v0.1.1 119 | with: 120 | to: ${{ secrets.CHANNEL_CHAT_ID }} 121 | token: ${{ secrets.BOT_TOKEN }} 122 | format: markdown 123 | message: | 124 | The result to 🎉the deployment of kite-server (*${{ env.BRANCH_NAME }}*): *${{ env.WORKFLOW_CONCLUSION }}* 125 | By *${{ github.actor }}*. Commit message: 126 | *${{ github.event.head_commit.message }}* 127 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Rust check 2 | 3 | on: 4 | push: { branches: [ master ] } 5 | pull_request: { branches: [ master ] } 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | check: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | - name: Checkout submodules 18 | run: git submodule update --init --force --recursive --remote 19 | 20 | - name: Install latest nightly 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | override: true 24 | toolchain: nightly 25 | - name: Install Protoc 26 | uses: arduino/setup-protoc@v1 27 | 28 | - name: Restore cargo packages 29 | uses: actions/cache@v3 30 | env: 31 | cache-name: cache-cargo-registry 32 | with: 33 | path: "~/.cargo/" 34 | key: ${{ runner.os }}-build-${{ env.cache-name }} 35 | restore-keys: ${{ runner.os }}-build-${{ env.cache-name }} 36 | 37 | - name: Run cargo check 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: check 41 | 42 | notification: 43 | runs-on: ubuntu-latest 44 | needs: [ check ] 45 | if: | 46 | github.repository == 'SIT-kite/kite-server' && 47 | always() 48 | steps: 49 | - uses: technote-space/workflow-conclusion-action@v2 50 | - uses: nelonoel/branch-name@v1.0.1 51 | - uses: benjlevesque/short-sha@v2.1 52 | id: short-sha 53 | with: { length: 7 } 54 | 55 | - name: 🤖 Build Success Notification 56 | uses: appleboy/telegram-action@v0.1.1 57 | with: 58 | to: ${{ secrets.CHANNEL_CHAT_ID }} 59 | token: ${{ secrets.BOT_TOKEN }} 60 | format: markdown 61 | message: | 62 | kite-server (*${{ env.BRANCH_NAME }}*) has just checked, *${{ env.WORKFLOW_CONCLUSION }}*. 63 | By *${{ github.actor }}*. Commit message: 64 | *${{ github.event.head_commit.message }}* 65 | -------------------------------------------------------------------------------- /.github/workflows/clean-cache.yml: -------------------------------------------------------------------------------- 1 | name: Clear cache 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | actions: write 8 | 9 | jobs: 10 | clear-cache: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Clear cache 14 | uses: actions/github-script@v6 15 | with: 16 | script: | 17 | console.log("About to clear") 18 | const caches = await github.rest.actions.getActionsCacheList({ 19 | owner: context.repo.owner, 20 | repo: context.repo.repo, 21 | }) 22 | for (const cache of caches.data.actions_caches) { 23 | console.log(cache) 24 | github.rest.actions.deleteActionsCacheById({ 25 | owner: context.repo.owner, 26 | repo: context.repo.repo, 27 | cache_id: cache.id, 28 | }) 29 | } 30 | console.log("Clear completed") 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | target/ 4 | .runtime-cache/ 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "proto"] 2 | path = proto 3 | url = https://github.com/SIT-kite/kite-proto.git 4 | [submodule "d4ocr-rust"] 5 | path = d4ocr-rust 6 | url = https://github.com/sunnysab/d4ocr-rust 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 2 | 3 | [workspace] 4 | members = [ 5 | "cache", 6 | "cache-macro", 7 | "captcha", 8 | "kite", 9 | "loader", 10 | "service-v3", 11 | "service-v2", 12 | "balance-updater", 13 | ] 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 上应小风筝(服务端) 2 | 3 | ## 概要 4 | 5 | 上应小风筝,旨在为 [上海应用技术大学](www.sit.edu.cn) 的学生提供校园信息整合与管理服务,本仓库管理并维护服务端代码。 6 | 7 | > 对于一位大学(本科)生而言,在校时间不过四年左右。而 “小风筝” 项目从 2020 年初疫情爆发时开始编写,到如今的 2023 已历经了 8 | > 3个年头了。项目 9 | > 团队的好友们所经历的 “从 ‘不太会使用 git 和 Github’、‘仅仅听说过 Rust 语言’,到如今虽然代码仍然写得不怎么样,但写出来的东西勉强能看、能用” 10 | > 的时光,令人倍感珍惜与怀念。 11 | 12 | ## 功能 13 | 14 | - [x] 电费查询 15 | - [x] 空教室查询 16 | - [ ] 二手闲置交易 17 | - [x] 入学信息查询 18 | - [ ] 失物招领 19 | 20 | 本版本废弃了一部分更新频率较少的接口,这部分数据将直接以静态方式([仓库](https://github.com/SIT-kite/kite-static))存储,以便于 21 | CDN 分发,同时节约成本、提高访问速度。 22 | 23 | ## 项目结构 24 | 25 | 目录 `proto` 包含了小风筝 gRPC 服务规范的若干通信消息与服务格式。其他项目所使用的 Rust 包如下表: 26 | 27 | | 包 | 说明 | 28 | |-------------|-----------------------------------| 29 | | cache | 读写用户查询时生成的缓存 | 30 | | cache-macro | 简化缓存使用的过程宏 | 31 | | kite | 基础库。提供了服务配置初始化,并提供了 Service trait | 32 | | loader | 程序入口 | 33 | | service-v3 | 小风筝 gRPC 服务 | 34 | 35 | ## 发展 36 | 37 | 让我来回顾一下项目发展的历史吧。 38 | 39 | 由于云计算厂商的服务器一般较为昂贵(他们一般希望 “帮助” 企业节约人工成本,因而设备租用价格并不低),加之经费报销的限制、个人习惯等因素,我们在 40 | 项目开工之初便决定尽可能地节约资源占用。开发之初缺乏有关开发和运营的经验,对 “并发数”、“用户数” 等概念和它背后的含义没有直观了解,我偏执地追 41 | 求着性能优化。我们希望它能在单核 1G 内存的机器上与数据库 PostgreSQL 流畅运行,并承载和选课阶段差不多的访问量。受 42 | [TechEmpower Framework Benchmarks](https://www.techempower.com/benchmarks/) 的影响,我们在最初的版本中开发中选择了 Rust 43 | 语言 44 | 及 [actix-web](https://github.com/actix/actix-web) 框架,彼时它正霸占排行榜的第一位,而且后几位全是它的小弟~ 45 | 46 | 2020 年中旬,我们开发出了 kite-server 第一版。9 月,提供 “新生入学查询”(即 “迎新”)服务,前端为微信小程序 “ 47 | [上应小风筝](https://github.com/SIT-kite/kite-microapp)”。当时的版本基于 actix-web 1 或 2,上线初发现: 48 | 49 | - reqwest 库对微信 API 服务器的连接无法正常释放,`nestat` 报告了大量处于 `CLOSE_WAIT` 状态的连接 50 | - 程序内存占用过大,猜测存在内存泄漏问题。内存占用 300 - 500M 左右时崩溃 51 | 52 | 在 Rust 中使用数据库和 SQL 这件事不好说难或简单。现成的库不少,社区尤为偏爱 PostgreSQL (现在我也是它的粉丝),但当时主流的 53 | ORM 只有 diesel 54 | (现在还有 SeaORM 等)。我曾为了 diesel 的一个泛型错误找了一天资料,也曾失手将周末写的代码删除。后来因为其对异步的支持不好,以及复杂查询导致 55 | “嵌套地狱” 的关系,项目便改用 sqlx 库了——裸写 SQL 语句也不错,至少有它帮忙把数据库返回的记录行转换成结构体。在 Web 56 | 后端开发过程中,文章匮乏 57 | 使人迷茫,连现成的项目也少得可怜:一般的例子都会带你写一组 TODO 接口,增删改查之后便草草结束——没有大一点的后端项目供我照葫芦画瓢,只能根据经验 58 | 不断地 “重构”,好让代码看起来更舒服一些。有一个 rust-admin 项目,是国人开发的,kite-server 照着它划了几瓢,可惜由于作者身体原因这个项目没 59 | 有进一步完善。 60 | 61 | 为了解决微信小程序访问校园网的问题,我们开发了 [kite-agent](https://github.com/SIT-kite/kite-agent) 程序,以期望它在校内提供中转服务。 62 | 具体是这样的:校园网内的机器(agent)先连接到 server, 后续由 server 将用户请求转发到 agent。绝大多数的 RPC 库都是 client 向 63 | server 建立连 64 | 接并发送请求,而这个(反向的)需求很冷门,一时竟无法解决,于是我基于 bincode (序列化和反序列化库)和 socket 完成了一个简易的 65 | RPC 工具。2021 66 | 年,在一番探索中,我兴奋地发现了 [tower](https://github.com/tower-rs/tower) 框架——它仿佛是为了我们这个需求而生的。暑假时我们就有了基于 67 | tower 的 kite-agent。tower 把服务与底层的连接进行了分离,支持各种中间件,让后端项目的结构上了个档次。从这一点,可以看出 Rust 68 | 的优点:底层。 69 | 据我所知,一些其他语言的框架不太能完成这样的操作,他们默认了 server 就是打开 socket 去监听的那一方。一定程度上来讲,这个需求也可以用 70 | frp、ssh 71 | 转发等方法实现,但当校园网内有多个 agent 实例时,这样的方式不太好做负载均衡。在后期我在 server 中支持了负载均衡的操作, 72 | server 可以将用户请求 73 | 随机地发给一个 agent,但也遇到了另一个问题:我需要一个结构去管理和 agent 的连接,当 agent 断开时,我希望自动从连接池中删除它。因为缺乏开发经验, 74 | 在所有权上和编译器斗智斗勇,这个功能始终没能实现。迫不得已,改成:发现连接中断(此时 send 会报错 broken pipe)时删除该连接。这便是 75 | Rust 基础不 76 | 牢带来的影响。 77 | 78 | 在 Rust 中做爬虫和网页解析更是麻烦。虽然有 Mozilla 开源的神器 html5ever,可 python 的 BeautifulSoup 谁也不想总是做那么底层的操作。直到 79 | 遇到了 scraper,这个库一定程度上简化了对 DOM 的操作,开发体验马马虎虎。 80 | 81 | 在 kite-server 1 和 kite-agent 开发期间,@zhangzqs 和 @B635 加入了开发,主要集中于 kite-agent 的编写。2021 年下半年的时候,我们的小 82 | 程序被校方要求下架,便改行做 App 开发,后端也要随之变动。后端趁机转向 v2 版本 ——基于油条哥的 poem-web 开发。总的来说,开发体验比现有的各个 83 | Rust Web 框架好一点。这期间,因为计划在 App 上直接连接校园网,后端便省去了与 agent 通信的环节,逐渐成为纯粹的 CRUD 机器。 84 | 85 | 在 Web 开发的过程中,慢慢体会到,接口是前后端交流的根据。于是,本分支下的 kite-server 3, 开始采用 tonic 框架,使用 gRPC 86 | 通信。在选型过程中,字节跳动团队开发的 volo 框架不断出现在视野中,但最终没有选择它原因是: 87 | 88 | 1. 生态较弱,最近没太多时间踩坑 89 | 2. 我更倾向于将 protobuf 生成的代码作为项目文件使用,而不是在 service 层使用类似 `incldue_proto!("xxx.proto")` 的方式编译 90 | proto 文件 91 | ——至少在 Clion 上,它将使得 IDE 提示完全失效,开发体验较差。而 tonic-build 给出了指定输出文件的方法。 92 | 93 | 本质上,本分支所管理的 v3 版本也是单纯的 CRUD 版本。可看的是,在这一版本中,将引入一个利用用户端网络和 gRPC 双向流去访问校园网的方法。同时, 94 | 该版本的项目组织与代码结构也更正式,可供编写类似项目的同仁参考。 95 | 96 | ## 环境配置 97 | 98 | 待完善 99 | 100 | ## 有关项目 101 | 102 | | 项目 | 说明 | 103 | |------------------------------------------------------------|---------------| 104 | | [kite-app](https://github.com/SIT-kite/kite-app) | App 前端 | 105 | | [zf-tools](https://github.com/sunnysab/zf-tools) | 正方教务系统爬虫 | 106 | | [kite-string](https://github.com/SIT-Kite/kite-string) | 校园网爬虫工具 | 107 | | [kite-agent](https://github.com/sunnysab/kite-agent) | 后端数据抓取工具(已废弃) | 108 | | [kite-protocol](https://github.com/SIT-Kite/kite-protocol) | 通信协议库(已废弃) | 109 | 110 | ## 如何贡献 111 | 112 | 算了,我都写麻了 113 | 114 | ## 开源协议 115 | 116 | [GPLv3](https://github.com/SIT-Kite/kite-server/blob/master/LICENSE) © 上海应用技术大学易班 小风筝团队 117 | 118 | 除此之外,您不能将本程序用于各类竞赛、毕业设计、论文等。 119 | 120 | -------------------------------------------------------------------------------- /balance-updater/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "balance-updater" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | kite = { path = "../kite" } 10 | 11 | serde = { version = "1.0", features = ["derive"] } 12 | serde_json = "1.0" 13 | anyhow = "1.0" 14 | chrono = { version = "0.4.23", features = ["serde"] } 15 | 16 | async-trait = "0.1.61" 17 | tokio = { version = "*", features = ["time"] } 18 | sqlx = { version = "0.7", default-features = false, features = ["runtime-tokio-rustls", "chrono", "postgres", "macros"] } 19 | reqwest = { version = "*", features = ["json"] } 20 | 21 | tracing = "0.1" 22 | once_cell = "1.17" -------------------------------------------------------------------------------- /balance-updater/src/cache.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use kite::cache; 20 | use kite::cache::SCOPE_BALANCE; 21 | 22 | pub fn clear_cache() { 23 | cache::get().erase_keys(SCOPE_BALANCE); 24 | } 25 | -------------------------------------------------------------------------------- /balance-updater/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use std::time::Duration; 20 | 21 | use anyhow::Result; 22 | use tokio::time; 23 | 24 | use kite::service::KiteModule; 25 | 26 | mod cache; 27 | mod pull; 28 | mod vaccum; 29 | 30 | pub struct BalanceUpdater {} 31 | 32 | #[async_trait::async_trait] 33 | impl KiteModule for BalanceUpdater { 34 | async fn run() { 35 | daemon().await.expect("BalanceUpdater exited."); 36 | } 37 | } 38 | 39 | async fn daemon() -> Result<()> { 40 | let db = kite::get_db(); 41 | 42 | let duration = Duration::from_secs(60 * 20); 43 | let mut interval = time::interval(duration); 44 | 45 | // Because it's a little hard to write async-callback in rust, I use one loop to handle these two 46 | // events. Maybe it can be rewrite in future. 47 | let mut i = 0; 48 | loop { 49 | interval.tick().await; 50 | if i % 72 == 0 { 51 | // 72 * 20min = one day 52 | if let Err(e) = vaccum::remove_outdated(&db).await { 53 | tracing::error!("Failed to remove outdated consumption record: {e}"); 54 | } 55 | i = 0; 56 | } 57 | // pull each 20min 58 | if let Err(e) = pull::pull_balance_list(&db).await { 59 | tracing::error!("Failed to pull balance list: {e}"); 60 | } 61 | i += 1; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /balance-updater/src/pull.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use anyhow::Result; 20 | use chrono::Local; 21 | use once_cell::sync::OnceCell; 22 | use serde::de::Error; 23 | use serde::Deserialize; 24 | use sqlx::{Acquire, Connection, PgPool}; 25 | use std::collections::{HashMap, HashSet}; 26 | use tokio::time::Instant; 27 | 28 | use super::cache::clear_cache; 29 | 30 | #[derive(Deserialize)] 31 | struct RawBalance { 32 | #[serde(rename(deserialize = "RoomName"), deserialize_with = "str2i32")] 33 | room: i32, 34 | #[serde(rename(deserialize = "Balance"), deserialize_with = "str2float")] 35 | total: f32, 36 | } 37 | 38 | type RoomNumber = i32; 39 | 40 | const INVALID_ROOM_NUMBER: RoomNumber = 0; 41 | 42 | fn str2i32<'de, D>(deserializer: D) -> Result 43 | where 44 | D: serde::Deserializer<'de>, 45 | { 46 | let s = String::deserialize(deserializer)?; 47 | // Some room numbers are start with "-" and used for testing, we will ignore them. 48 | if let Ok(n) = s.parse::() { 49 | if get_valid_room_set().contains(&n) { 50 | return Ok(n); 51 | } 52 | } 53 | Ok(INVALID_ROOM_NUMBER) 54 | } 55 | 56 | fn str2float<'de, D>(deserializer: D) -> Result 57 | where 58 | D: serde::Deserializer<'de>, 59 | { 60 | let s = String::deserialize(deserializer)?; 61 | s.parse::().map_err(Error::custom) 62 | } 63 | 64 | static VALID_ROOM_SET: OnceCell> = OnceCell::new(); 65 | 66 | async fn init_valid_room_set(db: &PgPool) -> Result<()> { 67 | #[derive(Eq, Hash, PartialEq, sqlx::FromRow)] 68 | struct ValidRoom { 69 | room: RoomNumber, 70 | } 71 | 72 | // Ignore pulling if VALID_ROOM_SET is set. 73 | if VALID_ROOM_SET.get().is_some() { 74 | return Ok(()); 75 | } 76 | let room_list: Vec = sqlx::query_as("SELECT id AS room FROM dormitory_room;") 77 | .fetch_all(db) 78 | .await?; 79 | 80 | let set = HashSet::from_iter(room_list.into_iter().map(|s| s.room)); 81 | VALID_ROOM_SET.set(set).expect("Failed to set VALID_ROOM_SET"); 82 | Ok(()) 83 | } 84 | 85 | fn get_valid_room_set() -> &'static HashSet { 86 | VALID_ROOM_SET 87 | .get() 88 | .expect("init_valid_room_set() should be called first.") 89 | } 90 | 91 | async fn request_room_balance() -> Result> { 92 | let client = reqwest::Client::new(); 93 | let mut params = HashMap::new(); 94 | 95 | static DATA_SOURCE_URL: &str = 96 | "https://xgfy.sit.edu.cn/unifri-flow/WF/Comm/ProcessRequest.do?DoType=DBAccess_RunSQLReturnTable"; 97 | 98 | params.insert("SQL", "select * from sys_room_balance;"); 99 | let response = client 100 | .post(DATA_SOURCE_URL) 101 | .header("Cookie", "FK_Dept=B1101") 102 | .form(¶ms) 103 | .send() 104 | .await?; 105 | 106 | response.json::>().await.map_err(Into::into) 107 | } 108 | 109 | async fn get_balance_list() -> Result> { 110 | let raw_response = request_room_balance().await?; 111 | let filter = |r: &RawBalance| r.room != INVALID_ROOM_NUMBER; 112 | let result = raw_response.into_iter().filter(filter).collect(); 113 | 114 | Ok(result) 115 | } 116 | 117 | async fn update_db(db: &PgPool, records: Vec) -> Result<()> { 118 | let current = Local::now(); 119 | let rooms: Vec = records.iter().map(|x| x.room).collect(); 120 | let balance: Vec = records.iter().map(|x| x.total).collect(); 121 | 122 | // Consumption is calculated by dormitory_balance_trigger on PostgreSQL. 123 | // Do not delete all data before any INSERT statement. 124 | // Here, we use a single SQL statement instead of a for loop to speed up updating process. 125 | sqlx::query( 126 | "INSERT INTO dormitory_balance 127 | (room, total_balance, ts) 128 | SELECT *, $3::timestamptz AS ts FROM UNNEST($1::int[], $2::float[]) 129 | ON CONFLICT (room) DO UPDATE SET total_balance = excluded.total_balance, ts = excluded.ts;", 130 | ) 131 | .bind(rooms) 132 | .bind(balance) 133 | .bind(current) 134 | .execute(db) 135 | .await 136 | .map(|_| ()) 137 | .map_err(Into::into) 138 | } 139 | 140 | async fn update_ranking(db: &PgPool) -> Result<()> { 141 | let mut transaction = db.begin().await?; 142 | 143 | sqlx::query("DELETE FROM dormitory_consumption_ranking;") 144 | .execute(&mut *transaction) 145 | .await?; 146 | sqlx::query( 147 | "INSERT INTO dormitory_consumption_ranking 148 | SELECT * FROM dormitory_do_rank();") 149 | .execute(&mut *transaction) 150 | .await?; 151 | 152 | transaction.commit().await.map_err(Into::into) 153 | } 154 | 155 | pub async fn pull_balance_list(db: &PgPool) -> Result<()> { 156 | init_valid_room_set(db).await?; 157 | 158 | let start = Instant::now(); 159 | let result = get_balance_list().await?; 160 | tracing::info!("get {} records, cost {}s", result.len(), start.elapsed().as_secs_f32()); 161 | 162 | let start = Instant::now(); 163 | let count = result.len(); 164 | update_db(db, result).await?; 165 | tracing::info!("save {} records, cost {}s", count, start.elapsed().as_secs_f32()); 166 | 167 | update_ranking(db).await?; 168 | clear_cache(); 169 | Ok(()) 170 | } 171 | -------------------------------------------------------------------------------- /balance-updater/src/vaccum.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use anyhow::Result; 20 | use sqlx::PgPool; 21 | 22 | pub async fn remove_outdated(db: &PgPool) -> Result<()> { 23 | sqlx::query("DELETE FROM dormitory_balance WHERE ts < current_timestamp - '8days'::interval;") 24 | .execute(db) 25 | .await?; 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /build-push.sh: -------------------------------------------------------------------------------- 1 | # 2 | # 上应小风筝 便利校园,一步到位 3 | # Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | 19 | cargo build --release 20 | cp target/release/loader kite-server-v3 21 | strip -s kite-server-v3 22 | 23 | scp kite-server-v3 kite@origin.kite.sunnysab.cn:~/kite-server-v3.new 24 | ssh kite@origin.kite.sunnysab.cn \ 25 | "systemctl --user stop kite3.service; \ 26 | mv kite-server-v3 kite-server-v3.old; \ 27 | mv kite-server-v3.new kite-server-v3; \ 28 | systemctl --user start kite3.service;" -------------------------------------------------------------------------------- /cache-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cache-macro" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | darling = "0.14" 10 | syn = { version = "1.0", features = ["full"] } 11 | quote = "1.0" 12 | proc-macro2 = "1.0" 13 | 14 | [lib] 15 | proc-macro = true -------------------------------------------------------------------------------- /cache-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use proc_macro::TokenStream; 20 | 21 | use darling::FromMeta; 22 | use proc_macro2::TokenStream as TokenStream2; 23 | use quote::quote; 24 | use syn::parse_macro_input; 25 | use syn::punctuated::Punctuated; 26 | use syn::token::Comma; 27 | 28 | const DEFAULT_CACHE_TIMEOUT: i64 = 3600; 29 | 30 | #[derive(Debug, darling::FromMeta)] 31 | struct CacheParameter { 32 | #[darling(default)] 33 | /// Cache timeout option (in second) 34 | timeout: Option, 35 | } 36 | 37 | fn parse_attribute(args: syn::AttributeArgs) -> CacheParameter { 38 | match CacheParameter::from_list(&args) { 39 | Ok(param) => param, 40 | Err(e) => { 41 | panic!("{}", e.to_string()); 42 | } 43 | } 44 | } 45 | 46 | fn parse_fn(item: syn::Item) -> syn::ItemFn { 47 | if let syn::Item::Fn(func) = item { 48 | func 49 | } else { 50 | panic!("You should only attach cache attribute to a function."); 51 | } 52 | } 53 | 54 | /// Adapted from: 55 | /// https://stackoverflow.com/questions/71480280/how-do-i-pass-arguments-from-a-generated-function-to-another-function-in-a-proce 56 | fn transform_args(args: &Punctuated) -> TokenStream2 { 57 | let idents_iter = args 58 | .iter() 59 | .filter_map(|arg| { 60 | if let syn::FnArg::Typed(pat_type) = arg { 61 | if let syn::Pat::Ident(pat_ident) = *pat_type.pat.clone() { 62 | return Some(pat_ident.ident); 63 | } 64 | } 65 | None 66 | }) 67 | .filter(|ident| { 68 | // Ignore db parameter 69 | let ident = ident.to_string(); 70 | ident != "db" && ident != "pool" 71 | }); 72 | 73 | let mut punctuated: Punctuated = Punctuated::new(); 74 | idents_iter.for_each(|ident| punctuated.push(ident)); 75 | 76 | TokenStream2::from(quote!(#punctuated)) 77 | } 78 | 79 | #[proc_macro_attribute] 80 | pub fn cache(args: TokenStream, item: TokenStream) -> TokenStream { 81 | let args = parse_macro_input!(args as syn::AttributeArgs); 82 | let item = parse_macro_input!(item as syn::Item); 83 | 84 | // Parse cache parameter 85 | let param = parse_attribute(args); 86 | let timeout = param.timeout.unwrap_or(DEFAULT_CACHE_TIMEOUT); 87 | 88 | // Parse function signature 89 | let syn::ItemFn { attrs, vis, sig, block } = parse_fn(item); 90 | // Parse function parameter 91 | let punctuated_args = transform_args(&sig.inputs); 92 | // Function return type 93 | let ret_type = if let syn::ReturnType::Type(_arrow, ty) = sig.output.clone() { 94 | *ty 95 | } else { 96 | panic!("Unexpected return type"); 97 | }; 98 | 99 | let result = TokenStream2::from(quote! { 100 | #(#attrs)* #vis #sig { 101 | use chrono::Duration; 102 | 103 | // Query cache 104 | if let Ok(Some(cache)) = kite::cache::cache_query!(key = #punctuated_args; timeout = Duration::seconds(#timeout)) { 105 | return Ok(cache); 106 | }; 107 | 108 | // If cache miss, do query operation 109 | let db_result: #ret_type = {#block}; 110 | let data = db_result?; 111 | 112 | // Save result to cache 113 | kite::cache::cache_save!(key = #punctuated_args; value = data.clone()); 114 | Ok(data) 115 | } 116 | }); 117 | result.into() 118 | } 119 | -------------------------------------------------------------------------------- /cache/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cache" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.68" 10 | once_cell = "1.17" 11 | serde = { version = "1", features = ["derive"] } 12 | chrono = { version = "0.4.23", features = ["serde"] } 13 | sled = "0.34" 14 | bincode = "2.0.0-rc.2" 15 | tracing = "0.1" -------------------------------------------------------------------------------- /cache/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use anyhow::Context; 20 | use chrono::{Duration, Local}; 21 | use once_cell::sync::OnceCell; 22 | 23 | pub const SCOPE_PUBLIC: u8 = 0; 24 | pub const SCOPE_BALANCE: u8 = 1; 25 | 26 | // TODO: Consider OS compatability 27 | const SLED_CACHE_PATH: &'static str = "./.runtime-cache/"; 28 | 29 | static CACHE: OnceCell = OnceCell::new(); 30 | 31 | trait CacheItemOperation { 32 | fn is_expired(&self) -> bool; 33 | } 34 | 35 | pub trait CacheOperation 36 | where 37 | T: bincode::Encode + bincode::Decode, 38 | { 39 | fn get(&self, key: &[u8], timeout: Duration) -> anyhow::Result>; 40 | 41 | fn set(&self, key: &[u8], value: T) -> anyhow::Result<()>; 42 | 43 | fn flush(&self) -> anyhow::Result<()>; 44 | } 45 | 46 | #[derive(Debug)] 47 | pub struct SledCache(sled::Db); 48 | 49 | #[derive(Debug, bincode::Decode, bincode::Encode)] 50 | struct CacheItem { 51 | /// Unix timestamp 52 | pub last_update: i64, 53 | /// Value 54 | pub value: T, 55 | } 56 | 57 | #[macro_export] 58 | macro_rules! this_type { 59 | ($v: expr) => {{ 60 | fn type_name_of(_: T) -> &'static str { 61 | std::any::type_name::() 62 | } 63 | let v = $v; 64 | type_name_of(v) 65 | }}; 66 | } 67 | 68 | #[macro_export] 69 | macro_rules! this_function { 70 | () => {{ 71 | fn f() {} 72 | fn type_name_of(_: T) -> &'static str { 73 | std::any::type_name::() 74 | } 75 | let name = type_name_of(f); 76 | &name[..name.len() - 3] 77 | }}; 78 | } 79 | 80 | /// BKDR String hash algorithm 81 | /// 82 | /// https://blog.csdn.net/djinglan/article/details/8812934 83 | pub fn bkdr_hash(initial_value: u64, s: &[u8]) -> u64 { 84 | let mut r = initial_value; 85 | for ch in s { 86 | let t = r.overflowing_mul(131).0; 87 | r = t.overflowing_add(*ch as u64).0; 88 | } 89 | r 90 | } 91 | 92 | pub fn u64_to_u8_array(mut n: u64) -> [u8; 8] { 93 | let mut result = [0; 8]; 94 | for byte in 0..8 { 95 | result[byte] = (n & 0xff) as u8; 96 | n >>= 8; 97 | } 98 | result 99 | } 100 | 101 | #[macro_export] 102 | macro_rules! cache_calc_key { 103 | ($($arg: expr),*) => {{ 104 | $crate::cache_calc_key!(scope = $crare::SCOPE_PUBLIC, $($arg),*) 105 | }}; 106 | (scope = $scope: expr; $($arg: expr),*) => {{ 107 | let scope = $scope; 108 | assert!($crate::this_type!(scope) == "u8"); 109 | 110 | let func: &str = $crate::this_function!(); 111 | let mut hash_key: u64 = $crate::bkdr_hash(0, func.as_bytes()); 112 | // TODO: Improve performance 113 | $( 114 | let parameter: String = format!("{:?}", $arg); 115 | hash_key = $crate::bkdr_hash(hash_key, parameter.as_bytes()); 116 | )* 117 | // Return cache key 118 | let mut result = [0u8; 9]; 119 | result[0] = scope; 120 | result[1..].copy_from_slice(&$crate::u64_to_u8_array(hash_key)); 121 | result 122 | }} 123 | } 124 | 125 | #[macro_export] 126 | macro_rules! cache_query { 127 | (key = $($arg: expr),*; timeout = $timeout: expr) => {{ 128 | $crate::cache_query!(key = $($arg),*; scope = $crate::SCOPE_PUBLIC; timeout = $timeout) 129 | }}; 130 | (key = $($arg: expr),*; scope = $scope: expr; timeout = $timeout: expr) => {{ 131 | use $crate::CacheOperation; 132 | 133 | let cache = $crate::get(); 134 | let cache_key = $crate::cache_calc_key!(scope = $scope; $($arg),*); 135 | 136 | cache.get(&cache_key, $timeout) 137 | }}; 138 | } 139 | 140 | #[macro_export] 141 | macro_rules! cache_save { 142 | (key = $($arg: expr),*; value = $value: expr) => {{ 143 | $crate::cache_save!(scope = $crate::SCOPE_PUBLIC; key = $($arg),*; value = $value) 144 | }}; 145 | (scope = $scope: expr; key = $($arg: expr),*; value = $value: expr) => {{ 146 | use $crate::CacheOperation; 147 | 148 | let cache = $crate::get(); 149 | let cache_key = $crate::cache_calc_key!(scope = $scope; $($arg),*); 150 | if let Err(e) = cache.set(&cache_key, $value) { 151 | tracing::warn!("failed to write data back to cache."); 152 | } 153 | }}; 154 | } 155 | 156 | #[macro_export] 157 | macro_rules! cache_erase { 158 | (key = $($arg: expr),*) => { 159 | $crate::cache_erase!(scope = $crate::SCOPE_PUBLIC; key = $($arg),*) 160 | }; 161 | (scope = $scope: expr; key = $($arg: expr),*) => {{ 162 | use $crate::CacheOperation; 163 | 164 | let cache = $crate::get(); 165 | let cache_key = $crate::cache_calc_key!($($arg),*); 166 | if let Err(e) = cache.erase(&cache_key) { 167 | tracing::warn!("failed to erase item in cache (key: {:?}): {}", cache_key, e); 168 | } 169 | }}; 170 | } 171 | 172 | impl SledCache { 173 | pub fn open(sled_path: &str) -> anyhow::Result { 174 | let db = sled::Config::new() 175 | .mode(sled::Mode::HighThroughput) 176 | .path(sled_path) 177 | .open()?; 178 | Ok(Self(db)) 179 | } 180 | 181 | /// Peek timestamp field without deserializing the hold CacheItem. 182 | /// 183 | /// Ref: https://github.com/bincode-org/bincode/blob/trunk/docs/spec.md 184 | fn peek_timestamp(value: &sled::IVec) -> i64 { 185 | // Assume that the machine use little endian 186 | assert!(value.len() >= 8); 187 | 188 | let mut result = 0i64; 189 | let ts_binary: &[u8] = &value.subslice(0, 8); 190 | 191 | // Following lines are equal to: 192 | // result |= ts_binary[0]; 193 | // result |= ts_binary[1] << 8; 194 | // result |= ts_binary[2] << 16; 195 | // ... 196 | for byte in 0..8 { 197 | result |= (ts_binary[byte] as i64) << ((byte << 3) as i64); 198 | } 199 | result 200 | } 201 | 202 | pub fn erase(&self, key: &[u8]) -> anyhow::Result<()> { 203 | self.0.remove(key)?; 204 | Ok(()) 205 | } 206 | 207 | pub fn erase_keys(&self, scope: u8) { 208 | let v = self.0.scan_prefix(&[scope]); 209 | v.keys().for_each(|key| { 210 | if let Ok(key) = key { 211 | let _ = self.0.remove(key); 212 | } 213 | }); 214 | } 215 | } 216 | 217 | impl CacheOperation for SledCache 218 | where 219 | T: bincode::Encode + bincode::Decode, 220 | { 221 | fn get(&self, key: &[u8], timeout: Duration) -> anyhow::Result> { 222 | let result = self.0.get(key); 223 | match result { 224 | Ok(Some(value)) => { 225 | let last_update = Self::peek_timestamp(&value); 226 | let now = Local::now().timestamp(); 227 | 228 | // Cache hit 229 | if now - last_update < timeout.num_seconds() { 230 | let config = bincode::config::legacy(); 231 | // Note: if cache key is conflict, the decode process will return an error. 232 | // Caller should not mark the query failed. 233 | bincode::decode_from_slice(&value, config) 234 | .map(|(item, _): (CacheItem, usize)| Some(item.value)) 235 | .map_err(Into::into) 236 | } else { 237 | // Cache expired 238 | // Remove the old and return none 239 | self.0 240 | .remove(key) 241 | .map(|_| None) 242 | .with_context(|| format!("Hit the expired item, failed to delete.")) 243 | } 244 | } 245 | Ok(None) => Ok(None), // Cache miss 246 | Err(e) => Err(Into::into(e)), // Operation failed. 247 | } 248 | } 249 | 250 | fn set(&self, key: &[u8], value: T) -> anyhow::Result<()> { 251 | let now = Local::now(); 252 | let config = bincode::config::legacy(); 253 | let item: CacheItem = CacheItem { 254 | last_update: now.timestamp(), 255 | value, 256 | }; 257 | 258 | let value = bincode::encode_to_vec(item, config).map_err(anyhow::Error::from)?; 259 | self.0 260 | .insert(key, value) 261 | .map(|_| ()) 262 | .with_context(|| format!("Failed to write cache")) 263 | } 264 | 265 | fn flush(&self) -> anyhow::Result<()> { 266 | self.0.flush().map(|_| ()).map_err(Into::into) 267 | } 268 | } 269 | 270 | pub fn initialize() { 271 | tracing::debug!("Opening cache database..."); 272 | 273 | let cache_handler = SledCache::open(SLED_CACHE_PATH) 274 | .with_context(|| format!("Cloud not open cache database: {}", SLED_CACHE_PATH)) 275 | .expect("Failed to initialize cache module."); 276 | 277 | CACHE.set(cache_handler).unwrap(); 278 | } 279 | 280 | pub fn get() -> &'static SledCache { 281 | CACHE.get().unwrap() 282 | } 283 | -------------------------------------------------------------------------------- /captcha/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "captcha" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | d4ocr-rust = { path = "../d4ocr-rust" } 10 | 11 | image = "0.24.1" 12 | anyhow = "1" 13 | once_cell = "1.17" 14 | tokio = { version = "*", features = ["rt-multi-thread", "sync"] } -------------------------------------------------------------------------------- /captcha/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use anyhow::{anyhow, Result}; 20 | use image::EncodableLayout; 21 | use once_cell::sync::OnceCell; 22 | use tokio::sync::{mpsc, oneshot}; 23 | 24 | use d4ocr_rust::{ImageSize, TransformationPipeline}; 25 | 26 | type AsyncChannelType = (Vec, oneshot::Sender>); 27 | 28 | const IMAGE_DEFAULT_WIDTH: usize = 173; 29 | const IMAGE_DEFAULT_HEIGHT: usize = 64; 30 | 31 | const QUEUE_SIZE: usize = 20; 32 | 33 | static MODEL: OnceCell = OnceCell::new(); 34 | static CHANNEL_SENDER: OnceCell> = OnceCell::new(); 35 | 36 | fn get() -> &'static TransformationPipeline { 37 | MODEL.get().expect("You should call init() or async_init() first.") 38 | } 39 | 40 | pub fn init() { 41 | let image_size = ImageSize { 42 | width: IMAGE_DEFAULT_WIDTH, 43 | height: IMAGE_DEFAULT_HEIGHT, 44 | }; 45 | 46 | if let Err(_) = MODEL.set(TransformationPipeline::new(image_size)) { 47 | panic!("Failed to load OCR model."); 48 | } 49 | } 50 | 51 | pub fn recognize(image: Vec) -> Result { 52 | let raw_image = image::load_from_memory(image.as_bytes())?; 53 | let gray_image = raw_image.to_luma8(); 54 | 55 | get().recognize(gray_image).map_err(|e| anyhow!("{e}")) 56 | } 57 | 58 | pub async fn async_init() { 59 | let _ = tokio::task::spawn_blocking(init).await; 60 | 61 | let (tx, mut rx) = mpsc::channel::(QUEUE_SIZE); 62 | std::thread::spawn(move || { 63 | while let Some((image, sender)) = rx.blocking_recv() { 64 | let result = recognize(image); 65 | let _ = sender.send(result); 66 | } 67 | }); 68 | 69 | let _ = CHANNEL_SENDER.set(tx); 70 | } 71 | 72 | pub async fn async_recognize(image: Vec) -> Result { 73 | let sender = CHANNEL_SENDER 74 | .get() 75 | .expect("Please use async_init() to initialize captcha-recognition module."); 76 | 77 | // Oneshot channel for response 78 | let (tx, rx) = oneshot::channel::>(); 79 | // Maybe sending result to a fail if the thread is busy and queue is full. 80 | sender.send((image, tx)).await?; 81 | rx.await? 82 | } 83 | -------------------------------------------------------------------------------- /kite.toml: -------------------------------------------------------------------------------- 1 | bind = "0.0.0.0:8000" 2 | secret = "secret" 3 | 4 | db = "postgresql://postgres:953bdfffb2e2fc54@localhost:2000/kite" 5 | db_conn = 2 6 | 7 | qweather_key = "xxx" -------------------------------------------------------------------------------- /kite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kite" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | cache = { path = "../cache" } 10 | cache-macro = { path = "../cache-macro" } 11 | 12 | # Asynchronous 13 | async-trait = "0.1.61" 14 | tokio = { version = "1", features = ["full"] } 15 | 16 | # Util, logging 17 | anyhow = "1.0.68" 18 | once_cell = "1.17" 19 | tracing = "0.1.37" 20 | regex = "1.7.1" 21 | regex-macro = "0.2.0" 22 | 23 | # Serialization and deserialization 24 | serde = { version = "1", features = ["derive"] } 25 | num-traits = "0.2" 26 | num-derive = "0.3" 27 | 28 | # Format & Types 29 | chrono = { version = "0.4.23", features = ["serde"] } 30 | uuid = { version = "1.2", features = ["serde", "v4"] } 31 | toml = "0.5" 32 | bincode = "2.0.0-rc.2" 33 | 34 | # SQL 35 | sqlx = { version = "0.7", default-features = false, features = ["runtime-tokio-rustls", "uuid", "chrono", "json", "postgres", "macros"] } 36 | -------------------------------------------------------------------------------- /kite/src/config.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use once_cell::sync::OnceCell; 20 | use serde::Deserialize; 21 | 22 | static CONFIG: OnceCell = OnceCell::new(); 23 | 24 | // Look and rename kite.example.toml 25 | const DEFAULT_CONFIG_PATH: &str = "kite.toml"; 26 | 27 | #[derive(Debug, Deserialize)] 28 | pub struct ServerConfig { 29 | /// Bind address with type "x.x.x.x:port" 30 | /// Usually "0.0.0.0:443" 31 | pub bind: String, 32 | /// JWT Secret for encrypt. 33 | pub secret: String, 34 | /// Database for postgresql. 35 | pub db: String, 36 | /// Max db conn 37 | pub db_conn: u32, 38 | 39 | /* External API */ 40 | /// QWeather.com API key. 41 | pub qweather_key: String, 42 | } 43 | 44 | fn get_config_path() -> String { 45 | std::env::var_os("KITE_CONFIG") 46 | .and_then(|s| Some(s.into_string().unwrap())) 47 | .unwrap_or(DEFAULT_CONFIG_PATH.to_string()) 48 | } 49 | 50 | /// Load the global configuration on startup. 51 | pub fn load_config() -> ServerConfig { 52 | let path = get_config_path(); 53 | 54 | std::fs::read_to_string(&path) 55 | .map_err(anyhow::Error::new) 56 | .and_then(|f| toml::from_str(&f).map_err(anyhow::Error::new)) 57 | .unwrap_or_else(|e| panic!("Failed to parse {:?}: {}", path, e)) 58 | } 59 | 60 | pub fn initialize() { 61 | tracing::debug!("Loading configuration..."); 62 | CONFIG 63 | .set(load_config()) 64 | .expect("Failed to load configuration file, which is kite.toml by default and can be set by KITE_CONFIG."); 65 | } 66 | 67 | pub fn get() -> &'static ServerConfig { 68 | CONFIG.get().expect("Config not initialized but you want to use.") 69 | } 70 | -------------------------------------------------------------------------------- /kite/src/db.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use once_cell::sync::OnceCell; 20 | use sqlx::postgres::PgPoolOptions; 21 | use sqlx::{Executor, PgPool}; 22 | 23 | use crate::config; 24 | 25 | static DB: OnceCell = OnceCell::new(); 26 | 27 | pub async fn initialize_db() { 28 | tracing::info!("Connecting to the main database..."); 29 | let pool = PgPoolOptions::new() 30 | .max_connections(config::get().db_conn) 31 | .after_connect(|conn, _| { 32 | Box::pin(async move { 33 | conn.execute("SET TIME ZONE 'Asia/Shanghai';").await?; 34 | Ok(()) 35 | }) 36 | }) 37 | .connect(config::get().db.as_str()) 38 | .await 39 | .expect("Could not initialize database pool."); 40 | 41 | tracing::info!("DB connected."); 42 | DB.set(pool).expect("Don't initialize db more than once."); 43 | } 44 | 45 | pub fn get_db() -> &'static PgPool { 46 | DB.get().expect("DB is not initialized!!!") 47 | } 48 | -------------------------------------------------------------------------------- /kite/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | pub use cache; 20 | pub use cache_macro::cache as cache_result; 21 | pub use db::get_db; 22 | 23 | pub mod config; 24 | pub mod db; 25 | pub mod model; 26 | pub mod service; 27 | -------------------------------------------------------------------------------- /kite/src/model.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | pub use template::*; 20 | 21 | pub mod badge; 22 | pub mod balance; 23 | pub mod board; 24 | pub mod classroom_browser; 25 | pub mod template; 26 | pub mod user; 27 | -------------------------------------------------------------------------------- /kite/src/model/badge.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use chrono::{DateTime, Local}; 20 | 21 | /// 识别结果 22 | #[derive(num_derive::ToPrimitive, num_derive::FromPrimitive)] 23 | enum ScanResult { 24 | /// 没有识别到校徽 25 | NoBadge = 1, 26 | /// 当日领福卡次数已达到限制 27 | ReachLimit = 2, 28 | /// 没有抽中 29 | NoCard = 3, 30 | /// 抽中了 31 | WinCard = 4, 32 | } 33 | 34 | /// 识别记录 35 | #[derive(serde::Serialize)] 36 | pub struct ScanRecord { 37 | /// 操作用户 ID 38 | pub uid: i32, 39 | /// 操作结果类型, 见 `ScanResult` 40 | pub result: i32, 41 | /// 卡片类型 (五种福卡之一) 42 | pub card: Option, 43 | /// 操作时间 44 | pub ts: DateTime, 45 | } 46 | 47 | #[derive(serde::Serialize, sqlx::FromRow)] 48 | pub struct Card { 49 | /// 卡片类型 (五种福卡之一) 50 | pub card: i32, 51 | /// 操作时间 52 | pub ts: DateTime, 53 | } 54 | -------------------------------------------------------------------------------- /kite/src/model/balance.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use anyhow::Result; 20 | use bincode::{Decode, Encode}; 21 | use chrono::{DateTime, Local}; 22 | use serde::Serialize; 23 | use sqlx::{FromRow, PgPool}; 24 | 25 | use crate as kite; 26 | 27 | #[derive(Clone, Encode, Decode, FromRow)] 28 | /// Electricity Balance for FengXian dormitory. 29 | pub struct ElectricityBalance { 30 | /// Room id in the format described in the doc. 31 | pub room: i32, 32 | /// Total available amount 33 | pub balance: f32, 34 | /// Last update time 35 | #[bincode(with_serde)] 36 | pub ts: DateTime, 37 | } 38 | 39 | /// Electricity usage statistics by day 40 | #[derive(Clone, Encode, Decode, Serialize, FromRow)] 41 | pub struct DailyElectricityBill { 42 | /// Date string in 'yyyy-mm-dd' 43 | pub date: String, 44 | /// Charge amount in estimation. 45 | pub charge: f32, 46 | /// Consumption amount in estimation. 47 | pub consumption: f32, 48 | } 49 | 50 | /// Electricity usage statistics by hour 51 | #[derive(Clone, Encode, Decode, Serialize, FromRow)] 52 | pub struct HourlyElectricityBill { 53 | /// Hour string in 'yyyy-mm-dd HH24:00' 54 | pub time: String, 55 | /// Charge amount in estimation. 56 | pub charge: f32, 57 | /// Consumption amount in estimation. 58 | pub consumption: f32, 59 | } 60 | 61 | /// Rank of recent-24hour consumption 62 | #[derive(Clone, Encode, Decode, Serialize, FromRow)] 63 | #[serde(rename_all = "camelCase")] 64 | pub struct RecentConsumptionRank { 65 | /// Consumption in last 24 hours. 66 | pub consumption: f32, 67 | /// Rank 68 | pub rank: i32, 69 | /// Total room count 70 | pub room_count: i32, 71 | } 72 | 73 | #[crate::cache_result(timeout = 900)] 74 | pub async fn get_latest_balance(pool: &PgPool, room: i32) -> Result> { 75 | sqlx::query_as( 76 | "SELECT room, total_balance AS balance, ts 77 | FROM dormitory_balance 78 | WHERE room = $1 79 | ORDER BY ts DESC 80 | LIMIT 1", 81 | ) 82 | .bind(room) 83 | .fetch_optional(pool) 84 | .await 85 | .map_err(Into::into) 86 | } 87 | 88 | #[crate::cache_result(timeout = 43200)] 89 | pub async fn get_bill_in_day(pool: &PgPool, room: i32, from: String, to: String) -> Result> { 90 | sqlx::query_as( 91 | "SELECT d.day AS date, COALESCE(records.charged_amount, 0.00) AS charge, ABS(COALESCE(records.used_amount, 0.00)) AS consumption 92 | FROM (SELECT to_char(day_range, 'yyyy-MM-dd') AS day FROM generate_series($1::date, $2::date, '1 day') AS day_range) d 93 | LEFT JOIN (SELECT * FROM dormitory_consumption_get_report_by_day($1::date, CAST($2::date + '1 day'::interval AS date), $3)) AS records 94 | ON d.day = records.day;") 95 | .bind(&from) 96 | .bind(&to) 97 | .bind(room) 98 | .fetch_all(pool) 99 | .await 100 | .map_err(Into::into) 101 | } 102 | 103 | #[crate::cache_result(timeout = 3600)] 104 | pub async fn get_bill_in_hour( 105 | pool: &PgPool, 106 | room: i32, 107 | from: DateTime, 108 | to: DateTime, 109 | ) -> Result> { 110 | sqlx::query_as( 111 | "SELECT h.hour AS time, COALESCE(records.charged_amount, 0.00) AS charge, ABS(COALESCE(records.used_amount, 0.00)) AS consumption 112 | FROM ( 113 | SELECT to_char(hour_range, 'yyyy-MM-dd HH24:00') AS hour 114 | FROM generate_series($1::timestamptz, $2::timestamptz, '1 hour') AS hour_range) h 115 | LEFT JOIN ( 116 | SELECT * FROM dormitory_consumption_get_report_by_hour($1::timestamptz, $2::timestamptz, $3)) AS records 117 | ON h.hour = records.hour;") 118 | .bind(from) 119 | .bind(to) 120 | .bind(room) 121 | .fetch_all(pool) 122 | .await 123 | .map_err(Into::into) 124 | } 125 | 126 | #[crate::cache_result(timeout = 3600)] 127 | pub async fn get_consumption_rank(pool: &PgPool, room: i32) -> Result> { 128 | // The value of 'SELECT COUNT(*) FROM dormitory_room;' is 4565, which will not change in a long future. 129 | // And be careful, room_count is of i32, while COUNT(*) returns a long long (int8) type. 130 | sqlx::query_as("SELECT room, consumption, rank, 4565 AS room_count FROM dormitory_consumption_ranking 131 | WHERE room = $1;") 132 | .bind(room) 133 | .fetch_optional(pool) 134 | .await 135 | .map_err(Into::into) 136 | } 137 | -------------------------------------------------------------------------------- /kite/src/model/board.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use chrono::{DateTime, Local}; 20 | 21 | #[derive(serde::Serialize, sqlx::FromRow)] 22 | pub struct Picture { 23 | /// Picture uuid 24 | pub id: super::Uuid, 25 | /// Updater 26 | pub uid: i32, 27 | /// Web path to origin image 28 | pub url: String, 29 | /// Web path to thumbnail image 30 | pub thumbnail: String, 31 | /// Upload time 32 | pub ts: DateTime, 33 | /// Extension 34 | pub ext: String, 35 | } 36 | -------------------------------------------------------------------------------- /kite/src/model/classroom_browser.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use bincode::{Decode, Encode}; 20 | use chrono::NaiveDate; 21 | 22 | #[derive(Encode, Decode, Clone, sqlx::FromRow)] 23 | pub struct Classroom { 24 | /// Room number 25 | pub title: String, 26 | /// Busy time flag 27 | pub busy_flag: i32, 28 | /// Room seats 29 | pub capacity: Option, 30 | } 31 | 32 | #[derive(Debug, Encode, Decode, Clone, sqlx::FromRow)] 33 | pub struct ClassroomQuery { 34 | /// The building to the classroom, for example, "一教" 35 | pub building: Option, 36 | /// The region to the classroom, for example, "A", "B" 37 | pub region: Option, 38 | /// Campus, for example, "徐汇校区" = 2 "奉贤校区" = 1 39 | pub campus: Option, 40 | /// Week index 41 | pub week: i32, 42 | /// Day index in a week 43 | pub day: i32, 44 | /// Want time in bits. 45 | /// 46 | /// When one bit set to 1, the corresponding course time is hoped. And the second place on the 47 | /// right presents the first class (8:20 - 9:55). 48 | /// For example, 110b to find the available classroom on class 1-2. 49 | pub want_time: Option, 50 | } 51 | 52 | /// Convert course index range string (like 1-9, 2-4) to binary, as a integer 53 | pub fn convert_range_string_to_binary(s: &str) -> i32 { 54 | let mut result = 0; 55 | 56 | // Make sure that the time index is in 1..=11 57 | let validate = |x: &str| -> i32 { 58 | x.parse::() 59 | .map(|x| if (1..=11).contains(&x) { x } else { 0 }) 60 | .unwrap_or(0) 61 | }; 62 | 63 | // Set time flag by seperated time index string, like "1-2" 64 | let set_time_flag = |duration: &str| { 65 | if let Some((min, max)) = duration.split_once('-') { 66 | for i in validate(min)..=validate(max) { 67 | result |= 1 << i; 68 | } 69 | } 70 | }; 71 | 72 | // Do conversion 73 | s.split(',').for_each(set_time_flag); 74 | result 75 | } 76 | 77 | /// Calculate week and day pair. 78 | pub fn calculate_week_day(term_begin: NaiveDate, date_to_calculate: NaiveDate) -> (i32, i32) { 79 | // Calculate the days between two dates 80 | let gap = date_to_calculate - term_begin; 81 | let gap_day: i32 = gap.num_days() as i32; 82 | // Calculate week and day index 83 | let week: i32 = gap_day / 7 + 1; 84 | let day: i32 = gap_day % 7 + 1; 85 | 86 | (week, day) 87 | } 88 | 89 | #[cfg(test)] 90 | mod test { 91 | use super::calculate_week_day; 92 | use super::convert_range_string_to_binary; 93 | 94 | #[test] 95 | fn test_convert_time_string() { 96 | // Normal cases 97 | assert_eq!(convert_time_string("1-11"), 4094); // 1111 1111 1110 98 | assert_eq!(convert_time_string("1-2"), 6); // 0110 99 | assert_eq!(convert_time_string(""), 0); 100 | assert_eq!(convert_time_string("1-2,3-4"), 30); // 0001 1110 101 | assert_eq!(convert_time_string("1-2,5-6"), 102); // 0110 0110 102 | assert_eq!(convert_time_string("1-2,5-6,9-11"), 3686); // 1110 0110 0110 103 | // Error cases 104 | assert_eq!(convert_time_string("1-a"), 0); 105 | assert_eq!(convert_time_string("1-2,1-b"), 6); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /kite/src/model/template.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | pub type Uuid = uuid::Uuid; 20 | 21 | const DEFAULT_PAGE_INDEX: i32 = 0; 22 | const DEFAULT_ITEM_COUNT: i32 = 20; 23 | 24 | pub enum PageSort { 25 | Asc, 26 | Desc, 27 | } 28 | 29 | pub struct PageView { 30 | pub size: i32, 31 | pub index: i32, 32 | pub sort: PageSort, 33 | } 34 | 35 | pub enum Gender { 36 | Male, 37 | Female, 38 | } 39 | 40 | impl Default for PageSort { 41 | fn default() -> Self { 42 | PageSort::Asc 43 | } 44 | } 45 | 46 | impl Default for PageView { 47 | fn default() -> Self { 48 | Self { 49 | size: DEFAULT_ITEM_COUNT, 50 | index: DEFAULT_PAGE_INDEX, 51 | sort: Default::default(), 52 | } 53 | } 54 | } 55 | 56 | impl PageView { 57 | /// Create a new page view structure 58 | pub fn new() -> Self { 59 | PageView::default() 60 | } 61 | /// Get validated index 62 | pub fn index(&self) -> i32 { 63 | if self.index > 0 { 64 | self.index 65 | } else { 66 | DEFAULT_PAGE_INDEX 67 | } 68 | } 69 | /// Get validated item count value 70 | pub fn count(&self, max_count: i32) -> i32 { 71 | if self.size < max_count { 72 | self.size 73 | } else { 74 | DEFAULT_ITEM_COUNT 75 | } 76 | } 77 | /// Calculate offset 78 | pub fn offset(&self, max_count: i32) -> i32 { 79 | let index = if self.index() > 0 { self.index() - 1 } else { 0 }; 80 | self.count(max_count) * index 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /kite/src/model/user.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use anyhow::Result; 20 | use chrono::{DateTime, Local}; 21 | use sqlx::PgPool; 22 | 23 | #[derive(Debug, sqlx::FromRow)] 24 | pub struct User { 25 | /// 用户 ID 26 | pub uid: i32, 27 | /// 用户学号 28 | pub account: String, 29 | /// 用户创建日期 30 | pub create_time: DateTime, 31 | /// 用户角色 32 | pub role: i32, 33 | /// 账户是否被禁用 34 | pub is_block: bool, 35 | } 36 | 37 | pub mod validate { 38 | use regex_macro::regex; 39 | 40 | pub fn check_username(account: &str) -> bool { 41 | let len = account.len() as i32; 42 | 43 | if ![4, 9, 10].contains(&len) { 44 | return false; 45 | } 46 | let regex = regex!(r"^((\d{2}6\d{6})|(\d{4})|(\d{6}[YGHE\d]\d{3}))$"); 47 | return regex.is_match(&account.to_uppercase()); 48 | } 49 | } 50 | 51 | pub async fn get(pool: &PgPool, uid: i32) -> Result> { 52 | sqlx::query_as("SELECT uid, account, create_time, role, is_block FROM user_account WHERE uid = $1 LIMIT 1;") 53 | .bind(uid) 54 | .fetch_optional(pool) 55 | .await 56 | .map_err(Into::into) 57 | } 58 | 59 | pub async fn query(pool: &PgPool, account: &str) -> Result> { 60 | sqlx::query_as("SELECT uid, account, create_time, role, is_block FROM user_account WHERE account = $1 LIMIT 1;") 61 | .bind(account) 62 | .fetch_optional(pool) 63 | .await 64 | .map_err(Into::into) 65 | } 66 | 67 | pub async fn create(pool: &PgPool, account: &str, name: &str) -> Result { 68 | sqlx::query_as( 69 | "INSERT INTO user_account (account, name) VALUES($1, $2) \ 70 | ON CONFLICT (account) DO UPDATE SET account = $1, name = $2 \ 71 | RETURNING uid, account, create_time, role, is_block;", 72 | ) 73 | .bind(account) 74 | .bind(name) 75 | .fetch_one(pool) 76 | .await 77 | .map_err(Into::into) 78 | } 79 | -------------------------------------------------------------------------------- /kite/src/service.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #[async_trait::async_trait] 20 | pub trait KiteModule { 21 | async fn run(); 22 | } 23 | -------------------------------------------------------------------------------- /kite3.service: -------------------------------------------------------------------------------- 1 | # SIT Tiny Kite, configuration file for systemd. 2 | # 3 | # by sunnysab (sunnysab.cn) 4 | # Created at: July 17, 2020 5 | # Last modified: Feb 4, 2023 6 | 7 | 8 | [Unit] 9 | Description=SIT Tiny Kite Server (version3) 10 | Wants=postgresql.service 11 | Before=nginx.service 12 | After=network.target 13 | 14 | [Service] 15 | Type=simple 16 | ExecStart=/usr/share/kite/kite-server-v3 17 | ExecStop=/bin/kill -2 $MAINPID 18 | PrivateTmp=true 19 | User=kite 20 | Group=kite 21 | # Ref: http://www.jinbuguo.com/systemd/systemd.exec.html#WorkingDirectory= 22 | WorkingDirectory=/usr/share/kite 23 | 24 | [Install] 25 | Alias=kite3 26 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /loader/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "loader" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | kite = { path = "../kite" } 10 | captcha = { path = "../captcha" } 11 | service-v2 = { path = "../service-v2" } 12 | service-v3 = { path = "../service-v3" } 13 | balance-updater = { path = "../balance-updater" } 14 | 15 | tokio = { version = "1", features = ["full"] } 16 | tracing = "0.1.37" 17 | tracing-subscriber = "0.3.16" 18 | 19 | [profile.release] 20 | opt-level = 3 21 | debug = true 22 | debug-assertions = false 23 | overflow-checks = false 24 | lto = true 25 | panic = 'unwind' 26 | incremental = false 27 | codegen-units = 1 28 | rpath = false -------------------------------------------------------------------------------- /loader/src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use kite::cache; 20 | use kite::config; 21 | use kite::db; 22 | use kite::service::KiteModule; 23 | 24 | #[tokio::main] 25 | async fn main() { 26 | tracing_subscriber::fmt().with_max_level(tracing::Level::INFO).init(); 27 | tracing::info!("Starting..."); 28 | 29 | config::initialize(); 30 | cache::initialize(); 31 | 32 | db::initialize_db().await; 33 | captcha::async_init().await; 34 | 35 | tokio::join! { 36 | service_v2::ServerHttp::run(), 37 | service_v3::ServerV3::run(), 38 | balance_updater::BalanceUpdater::run(), 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | 3 | ignore = ["service-v3/src/service/gen/"] -------------------------------------------------------------------------------- /service-v2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "service-v2" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | kite = { path = "../kite" } 10 | captcha = { path = "../captcha" } 11 | 12 | anyhow = "1.0" 13 | async-trait = "0.1" 14 | serde_json = "1.0" 15 | chrono = { version = "0.4.23", features = ["serde"] } 16 | serde = { version = "1.0", features = ["derive"] } 17 | base64 = "0.13" 18 | tracing = "0.1" 19 | 20 | poem = "1.3" 21 | sqlx = { version = "0.7", default-features = false, features = ["runtime-tokio-rustls", "chrono", "postgres", "macros"] } -------------------------------------------------------------------------------- /service-v2/src/captcha.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use poem::handler; 20 | use poem::web::Json; 21 | 22 | use crate::error::Result; 23 | use crate::response::ApiResponse; 24 | 25 | #[handler] 26 | pub async fn recognize_captcha(body: String) -> Result> { 27 | async fn inner_recognize(image_in_base64: String) -> anyhow::Result { 28 | let image = base64::decode(image_in_base64)?; 29 | let text = captcha::async_recognize(image).await?; 30 | 31 | tracing::info!("Captcha result: {text}"); 32 | Ok(text) 33 | } 34 | 35 | let result = if !body.is_empty() { 36 | match inner_recognize(body).await { 37 | Ok(text) => ApiResponse::normal(text), 38 | Err(e) => ApiResponse::fail(1, e.to_string()), 39 | } 40 | } else { 41 | let err = "No request body provided.".to_string(); 42 | ApiResponse::fail(1, err) 43 | }; 44 | 45 | Ok(Json(result.into())) 46 | } 47 | -------------------------------------------------------------------------------- /service-v2/src/electricity.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use std::ops::Sub; 20 | 21 | use chrono::{Date, Local}; 22 | use chrono::{DateTime, Duration}; 23 | use poem::handler; 24 | use poem::web::{Data, Json, Path, Query}; 25 | use serde::Serialize; 26 | use sqlx::PgPool; 27 | 28 | use kite::model::balance as model; 29 | use kite::model::balance::RecentConsumptionRank; 30 | 31 | use crate::error::Result; 32 | use crate::response::ApiResponse; 33 | 34 | #[derive(Serialize)] 35 | /// Electricity Balance for FengXian dormitory. 36 | pub struct ElectricityBalance { 37 | /// Room id in the format described in the doc. 38 | pub room: i32, 39 | /// Total available amount 40 | pub balance: f32, 41 | /// Remaining power level 42 | pub power: f32, 43 | /// Last update time 44 | pub ts: DateTime, 45 | } 46 | 47 | impl Into for model::ElectricityBalance { 48 | fn into(self) -> ElectricityBalance { 49 | let model::ElectricityBalance { room, balance, ts } = self; 50 | ElectricityBalance { 51 | room, 52 | balance, 53 | power: self.balance / 0.6, 54 | ts, 55 | } 56 | } 57 | } 58 | 59 | #[handler] 60 | pub async fn query_room_balance(pool: Data<&PgPool>, Path(room): Path) -> Result> { 61 | let data: Option = model::get_latest_balance(&pool, room).await?.map(Into::into); 62 | 63 | let content: serde_json::Value = if let Some(data) = data { 64 | ApiResponse::normal(data).into() 65 | } else { 66 | ApiResponse::<()>::fail(404, "No such room.".to_string()).into() 67 | }; 68 | Ok(Json(content)) 69 | } 70 | 71 | #[handler] 72 | pub async fn query_room_consumption_rank( 73 | pool: Data<&PgPool>, 74 | Path(room): Path, 75 | ) -> Result> { 76 | let data = model::get_consumption_rank(&pool, room).await? 77 | .unwrap_or_else(|| RecentConsumptionRank { 78 | consumption: 0.0, 79 | rank: 4565, 80 | room_count: 4565, 81 | }); 82 | let response: serde_json::Value = ApiResponse::normal(data).into(); 83 | 84 | Ok(Json(response)) 85 | } 86 | 87 | #[derive(serde::Deserialize)] 88 | pub struct DateRange { 89 | start: Option, 90 | end: Option, 91 | } 92 | 93 | #[handler] 94 | pub async fn query_room_bills_by_day( 95 | pool: Data<&PgPool>, 96 | Path(room): Path, 97 | Query(parameters): Query, 98 | ) -> Result> { 99 | let today = chrono::Local::today(); 100 | let to_str = |x: Date| x.format("%Y-%m-%d").to_string(); 101 | 102 | let start_date = parameters.start.unwrap_or_else(|| to_str(today.sub(Duration::days(7)))); 103 | let end_date = parameters.end.unwrap_or_else(|| to_str(today)); 104 | 105 | let data = model::get_bill_in_day(&pool, room, start_date, end_date).await?; 106 | let response: serde_json::Value = ApiResponse::normal(data).into(); 107 | 108 | Ok(Json(response)) 109 | } 110 | 111 | #[handler] 112 | pub async fn query_room_bills_by_hour(pool: Data<&PgPool>, Path(room): Path) -> Result> { 113 | let now = chrono::Local::now(); 114 | 115 | let start_time = now.sub(Duration::days(1)); 116 | let end_time = now; 117 | 118 | let data = model::get_bill_in_hour(&pool, room, start_time, end_time).await?; 119 | let response: serde_json::Value = ApiResponse::normal(data).into(); 120 | 121 | Ok(Json(response)) 122 | } 123 | -------------------------------------------------------------------------------- /service-v2/src/error.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use poem::error::ResponseError; 20 | use poem::http::StatusCode; 21 | use serde::ser::StdError; 22 | 23 | pub type Result = std::result::Result; 24 | 25 | #[derive(Debug, serde::Serialize)] 26 | pub struct ApiError { 27 | pub code: u16, 28 | pub msg: Option, 29 | } 30 | 31 | impl std::fmt::Display for ApiError { 32 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 | write!(f, "{}", serde_json::to_string(&self).unwrap()) 34 | } 35 | } 36 | 37 | impl StdError for ApiError {} 38 | 39 | impl ResponseError for ApiError { 40 | fn status(&self) -> StatusCode { 41 | StatusCode::OK 42 | } 43 | 44 | /// Convert this error to a HTTP response. 45 | fn as_response(&self) -> poem::Response 46 | where 47 | Self: StdError + Send + Sync + 'static, 48 | { 49 | poem::Response::builder() 50 | .status(self.status()) 51 | .header("Content-Type", "application/json") 52 | .body(serde_json::to_string(&self).unwrap()) 53 | } 54 | } 55 | 56 | impl ApiError { 57 | pub fn new(sub_err: T) -> Self { 58 | Self { 59 | code: 1, 60 | msg: Some(sub_err.to_string()), 61 | } 62 | } 63 | 64 | pub fn custom(code: u16, msg: &str) -> Self { 65 | Self { 66 | code, 67 | msg: Some(msg.to_string()), 68 | } 69 | } 70 | } 71 | 72 | impl From for ApiError { 73 | fn from(value: anyhow::Error) -> Self { 74 | Self { 75 | code: 1, 76 | msg: Some(value.to_string()), 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /service-v2/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use poem::middleware::AddData; 20 | use poem::{get, listener::TcpListener, post, EndpointExt, Route}; 21 | 22 | use kite::get_db; 23 | use kite::service::KiteModule; 24 | 25 | mod response; 26 | 27 | mod captcha; 28 | mod electricity; 29 | mod error; 30 | 31 | pub struct ServerHttp; 32 | 33 | #[async_trait::async_trait] 34 | impl KiteModule for ServerHttp { 35 | async fn run() { 36 | http_service().await.expect("Failed to run http_service") 37 | } 38 | } 39 | 40 | async fn http_service() -> Result<(), std::io::Error> { 41 | let route = Route::new() 42 | .nest( 43 | "/electricity", 44 | Route::new() 45 | .at("/room/:room", get(electricity::query_room_balance)) 46 | .at("/room/:room/rank", get(electricity::query_room_consumption_rank)) 47 | .at("/room/:room/bill/days", get(electricity::query_room_bills_by_day)) 48 | .at("/room/:room/bill/hours", get(electricity::query_room_bills_by_hour)), 49 | ) 50 | .nest("/ocr", Route::new().at("/captcha", post(captcha::recognize_captcha))); 51 | 52 | let app = route.with(AddData::new(get_db().clone())); 53 | poem::Server::new(TcpListener::bind("127.0.0.1:3000")).run(app).await 54 | } 55 | -------------------------------------------------------------------------------- /service-v2/src/response.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use serde::Serialize; 20 | 21 | #[derive(serde::Serialize)] 22 | pub struct ApiResponse { 23 | code: u16, 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | msg: Option, 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | data: Option, 28 | } 29 | 30 | impl ApiResponse { 31 | pub fn new(code: u16, msg: Option, data: Option) -> Self { 32 | Self { code, msg, data } 33 | } 34 | 35 | pub fn normal(data: T) -> Self { 36 | Self::new(0, None, Some(data)) 37 | } 38 | 39 | pub fn empty() -> Self { 40 | Self::new(0, None, None) 41 | } 42 | 43 | pub fn fail(code: u16, msg: String) -> Self { 44 | Self::new(code, Some(msg), None) 45 | } 46 | } 47 | 48 | impl Into for ApiResponse { 49 | fn into(self) -> serde_json::Value { 50 | serde_json::to_value(&self).unwrap() 51 | } 52 | } 53 | 54 | impl ToString for ApiResponse 55 | where 56 | T: Serialize, 57 | { 58 | // Serialize 59 | fn to_string(&self) -> String { 60 | if let Ok(body_json) = serde_json::to_string(&self) { 61 | return body_json; 62 | } 63 | String::from("Critical: Could not serialize error message.") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /service-v3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "service-v3" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | kite = { path = "../kite" } 8 | captcha = { path = "../captcha" } 9 | 10 | anyhow = "1" 11 | async-trait = "0.1" 12 | tokio = { version = "1", features = ["full"] } 13 | tokio-rustls = "0.23.4" 14 | tokio-stream = "0.1.11" 15 | webpki-roots = "0.22.6" 16 | hyper = { version = "0.14.23", default-features = false, features = ["http1", "client"] } 17 | rustls = "0.20.8" 18 | tracing = "0.1" 19 | prost = "0.11.6" 20 | tonic = "0.8.3" 21 | tonic-reflection = "0.6" 22 | tower = { version = "0.4", features = ["full"] } 23 | tower-http = { version = "0.3", features = ["trace"] } 24 | http = "0.2" 25 | sqlx = { version = "0.7", default-features = false, features = ["runtime-tokio-rustls", "uuid", "chrono", "json", "postgres", "macros"] } 26 | jsonwebtoken = "8.2" 27 | bincode = { version = "2.0.0-rc.2", features = ["serde"] } 28 | serde = { version = "1", features = ["derive"] } 29 | chrono = { version = "0.4.23", features = ["serde"] } 30 | num-traits = "0.2" 31 | num-derive = "0.3" 32 | once_cell = "1.17.0" 33 | scraper = "0.14.0" 34 | base64 = "0.21.0" 35 | block-modes = "0.7" 36 | aes = "0.6" 37 | bytes = "1.3.0" 38 | percent-encoding = "2.2.0" 39 | prost-types = "0.11.6" 40 | regex = "1.7.1" 41 | regex-macro = "0.2.0" 42 | 43 | [build-dependencies] 44 | tonic-build = "0.8" 45 | -------------------------------------------------------------------------------- /service-v3/build.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | fn main() { 20 | let out_dir = std::path::PathBuf::from("src/service/gen/"); 21 | println!("Run build.rs !!!"); 22 | 23 | let proto_files = &[ 24 | // Basic 25 | "../proto/template.proto", 26 | "../proto/token.proto", 27 | "../proto/typing.proto", 28 | // Test 29 | "../proto/ping.proto", 30 | // Service 31 | "../proto/badge.proto", 32 | "../proto/balance.proto", 33 | "../proto/board.proto", 34 | "../proto/captcha.proto", 35 | "../proto/classroom_browser.proto", 36 | "../proto/exception.proto", 37 | "../proto/freshman.proto", 38 | "../proto/game.proto", 39 | "../proto/user.proto", 40 | "../proto/yellow_page.proto", 41 | ]; 42 | for &path in proto_files { 43 | println!("cargo:rerun-if-changed=\"{path}\""); 44 | } 45 | 46 | tonic_build::configure() 47 | .build_server(true) 48 | .build_client(false) 49 | .file_descriptor_set_path("../target/compiled-descriptor.bin") 50 | .out_dir(out_dir) 51 | .compile(proto_files, &["../proto"]) 52 | .unwrap(); 53 | } 54 | -------------------------------------------------------------------------------- /service-v3/src/authserver.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | pub use client::Session; 20 | pub use portal::{Credential, Portal, PortalConnector}; 21 | pub use tls::get as tls_get; 22 | 23 | mod client; 24 | mod portal; 25 | mod tls; 26 | 27 | pub mod constants { 28 | pub const SERVER_NAME: &str = "authserver.sit.edu.cn"; 29 | /// 登录页. 第一次请求使用 GET 方法, 发送表单使用 POST 方法. 30 | pub const LOGIN_URI: &str = "/authserver/login"; 31 | /// 访问登录后的信息页 32 | pub const AUTH_SERVER_HOME_URI: &str = "/authserver/index.do"; 33 | /// 检查该用户登录是否需要验证码 34 | pub const NEED_CAPTCHA_URI: &str = "/authserver/needCaptcha.html"; 35 | /// 登录时使用的 User-Agent 36 | pub const DESKTOP_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36 Edg/97.0.1072.69"; 37 | /// 验证码 38 | pub const CAPTCHA_URI: &str = "/authserver/captcha.html"; 39 | } 40 | -------------------------------------------------------------------------------- /service-v3/src/authserver/client.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use std::collections::HashMap; 20 | 21 | use anyhow::Result; 22 | use bytes::{BufMut, Bytes, BytesMut}; 23 | use hyper::body::HttpBody; 24 | use hyper::client::conn; 25 | use hyper::{Body, Method, Response, StatusCode}; 26 | use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; 27 | use tokio::io::{AsyncRead, AsyncWrite}; 28 | use tokio::sync::oneshot; 29 | use tokio_rustls::client::TlsStream; 30 | 31 | use super::constants::*; 32 | 33 | #[derive(Default)] 34 | struct CookieJar { 35 | pub inner: HashMap, 36 | } 37 | 38 | impl CookieJar { 39 | fn parse_line(cookie: &str) -> Option<(&str, &str)> { 40 | // JSESSIONID=xSiUKpqm0lmjhDXB41_hhyxiNUa69u4xMnHkFOFS61E6VZ6Osp7S!-1266297679; path=/; HttpOnly 41 | cookie.split_once(';').and_then(|s| s.0.split_once('=')) 42 | } 43 | 44 | pub fn append(&mut self, cookie: &str) { 45 | if let Some((k, v)) = Self::parse_line(cookie) { 46 | // This method will override the old one if k already exists. 47 | self.inner.insert(k.to_string(), v.to_string()); 48 | } 49 | } 50 | 51 | pub fn to_string(&self) -> Option { 52 | if self.inner.is_empty() { 53 | return None; 54 | } 55 | let result = self 56 | .inner 57 | .iter() 58 | .fold(String::new(), |s, (k, v)| s + &*format!("{}={};", k, v)); 59 | return Some(result); 60 | } 61 | 62 | pub fn clear(&mut self) { 63 | self.inner.clear(); 64 | } 65 | } 66 | 67 | /// 会话. 用于在 Http 连接上虚拟若干不同用户的会话. 68 | pub struct Session 69 | where 70 | T: AsyncRead + AsyncWrite + Send + Unpin + 'static, 71 | { 72 | /// 会话用的连接 73 | sender: conn::SendRequest, 74 | /// Cookie 存储 75 | cookie_jar: CookieJar, 76 | /// receiver to released TLS stream 77 | released_rx: oneshot::Receiver>, 78 | } 79 | 80 | impl Session 81 | where 82 | T: AsyncRead + AsyncWrite + Send + Unpin + 'static, 83 | { 84 | /// Initialize a TLS connection and wait for next operations 85 | pub async fn create(stream: TlsStream) -> Result> { 86 | // Client hello !! 87 | let (sender, connection) = conn::handshake(stream).await?; 88 | 89 | // A task to poll the connection and drive the HTTP state 90 | // If the connection closed, return tls stream. 91 | let (tx, rx) = oneshot::channel(); 92 | tokio::spawn(async move { 93 | // Connection: close is set on the last request, the /login post, to cause the server to 94 | // close TLS layer connection actively. 95 | // So will the without_shutdown method return. 96 | let result = connection.without_shutdown().await; 97 | if let Ok(part) = result { 98 | let _ = tx.send(part.io); 99 | } 100 | }); 101 | let result = Session { 102 | sender, 103 | cookie_jar: CookieJar::default(), 104 | released_rx: rx, 105 | }; 106 | Ok(result) 107 | } 108 | 109 | /// A simple wrapper, which can send a HTTP(S) request 110 | async fn request( 111 | &mut self, 112 | method: Method, 113 | uri: &str, 114 | text_payload: Option, 115 | header: Vec<(String, String)>, 116 | ) -> Result> { 117 | let mut builder = http::Request::builder() 118 | .method(method) 119 | .uri(uri) 120 | .header("Host", SERVER_NAME) 121 | .header("User-Agent", DESKTOP_USER_AGENT); 122 | for (k, v) in header { 123 | builder = builder.header(k, v); 124 | } 125 | 126 | if let Some(cookie) = self.cookie_jar.to_string() { 127 | builder = builder.header("Cookie", cookie); 128 | } 129 | let body = text_payload.map(Body::from).unwrap_or_else(|| Body::empty()); 130 | let request = builder.body(body)?; 131 | 132 | /* Send request and receive header*/ 133 | let response = self.sender.send_request(request).await?; 134 | let (header, mut body) = response.into_parts(); 135 | // Store cookies 136 | if let Some(cookies) = header.headers.get("Set-Cookie") { 137 | self.cookie_jar.append(cookies.to_str().unwrap()); 138 | } 139 | // Pull data chunks 140 | let mut content = BytesMut::new(); 141 | while let Some(chunk) = body.data().await { 142 | let chunk = chunk?; 143 | content.put(chunk); 144 | } 145 | let content = Bytes::from(content); 146 | let response = Response::from_parts(header, content); 147 | Ok(response) 148 | } 149 | 150 | pub async fn get(&mut self, url: &str) -> Result> { 151 | self.request(Method::GET, url, None, vec![]).await 152 | } 153 | 154 | pub async fn get_with_redirection(&mut self, url: &str, max_direction: u8) -> Result> { 155 | let mut count = 0u8; 156 | let mut target = String::from(url); 157 | let mut response: Response = Default::default(); 158 | 159 | assert!(max_direction > count); 160 | while count < max_direction { 161 | count += 1; 162 | 163 | response = self.get(&target).await?; 164 | let status = response.status(); 165 | 166 | if status == StatusCode::FOUND || status == StatusCode::MOVED_PERMANENTLY { 167 | let new_target = response.headers().get("Location").unwrap(); 168 | target = new_target.to_str()?.to_string(); 169 | } else { 170 | break; 171 | } 172 | } 173 | if count == max_direction { 174 | Err(anyhow::anyhow!("Max redirection count exceeds.")) 175 | } else { 176 | Ok(response) 177 | } 178 | } 179 | 180 | pub async fn post( 181 | &mut self, 182 | url: &str, 183 | form: Vec<(&str, &str)>, 184 | header: Vec<(&str, &str)>, 185 | ) -> Result> { 186 | let content = if !form.is_empty() { 187 | let s = form 188 | .into_iter() 189 | .map(|(k, v)| (k.to_string(), utf8_percent_encode(v, NON_ALPHANUMERIC).to_string())) 190 | .fold(String::new(), |c, (k, v)| c + &format!("{}={}&", k, v)); 191 | Some(s) 192 | } else { 193 | None 194 | }; 195 | let header = header 196 | .into_iter() 197 | .map(|(k, v)| (k.to_string(), v.to_string())) 198 | .collect(); 199 | 200 | self.request(Method::POST, url, content, header).await 201 | } 202 | 203 | pub async fn request_close_connection(&mut self) -> Result<()> { 204 | let header = vec![("Connection".to_string(), "close".to_string())]; 205 | self.request(Method::GET, "/", None, header).await?; 206 | Ok(()) 207 | } 208 | 209 | pub fn clear_cookie(&mut self) { 210 | self.cookie_jar.clear(); 211 | } 212 | 213 | pub async fn wait_for_shutdown(self) -> Result> { 214 | self.released_rx.await.map_err(Into::into) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /service-v3/src/authserver/portal.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use anyhow::Result; 20 | use base64::Engine; 21 | use http::StatusCode; 22 | use scraper::{Html, Selector}; 23 | use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; 24 | 25 | use super::constants::*; 26 | use super::Session; 27 | 28 | #[derive(Clone)] 29 | pub struct Credential { 30 | /// 学号 31 | pub account: String, 32 | /// OA密码 33 | pub password: String, 34 | } 35 | 36 | impl Credential { 37 | pub fn new(account: String, password: String) -> Credential { 38 | Credential { account, password } 39 | } 40 | } 41 | 42 | pub struct PortalConnector { 43 | credential: Option, 44 | } 45 | 46 | impl PortalConnector { 47 | pub fn new() -> Self { 48 | Self { credential: None } 49 | } 50 | 51 | pub fn user(self, credential: Credential) -> Self { 52 | Self { 53 | credential: Some(credential), 54 | ..self 55 | } 56 | } 57 | 58 | pub async fn bind(self, stream: T) -> Result> 59 | where 60 | T: AsyncRead + AsyncWrite + Send + Unpin + 'static, 61 | { 62 | let credential = self.credential.expect("Credential is required."); 63 | 64 | // Prepare client configuration which used to handshake 65 | let config = crate::authserver::tls_get().clone(); 66 | let connector = tokio_rustls::TlsConnector::from(config); 67 | let server_name = "authserver.sit.edu.cn".try_into().unwrap(); 68 | 69 | // Bind IO with TLS config (do some TLS initializing operation) 70 | // Maybe the connect function should not be a async function? 71 | let stream = connector.connect(server_name, stream).await.unwrap(); 72 | let session = Session::create(stream).await?; 73 | 74 | Ok(Portal { credential, session }) 75 | } 76 | } 77 | 78 | /// 统一认证模块 79 | pub struct Portal 80 | where 81 | T: AsyncRead + AsyncWrite + Send + Unpin + 'static, 82 | { 83 | credential: Credential, 84 | /// 登录会话 85 | session: Session, 86 | } 87 | 88 | /// Search in text by regex, and return the first group. 89 | #[macro_export] 90 | macro_rules! regex_find { 91 | ($text: expr, $pattern: expr) => {{ 92 | let re = regex::Regex::new($pattern).unwrap(); 93 | re.captures($text).map(|r| r[1].to_string()) 94 | }}; 95 | } 96 | 97 | struct IndexParameter { 98 | aes_key: String, 99 | lt: String, 100 | } 101 | 102 | impl Portal 103 | where 104 | T: AsyncRead + AsyncWrite + Send + Unpin + 'static, 105 | { 106 | /// Check whether captcha is need or not. 107 | async fn check_need_captcha(&mut self, account: &str) -> Result { 108 | let url = format!("{}?username={}&pwdEncrypt2=pwdEncryptSalt", NEED_CAPTCHA_URI, account); 109 | let response = self.session.get(&url).await?; 110 | 111 | let content = response.body(); 112 | Ok(content.eq_ignore_ascii_case(b"true")) 113 | } 114 | 115 | /// Fetch captcha image. 116 | async fn fetch_captcha(&mut self) -> Result> { 117 | let response = self.session.get(CAPTCHA_URI).await?; 118 | let content = response.body(); 119 | return Ok(content.to_vec()); 120 | } 121 | 122 | /// Identify captcha images 123 | async fn recognize_captcha(&mut self, image_content: Vec) -> Result { 124 | captcha::async_recognize(image_content).await 125 | } 126 | 127 | pub async fn get_person_name(&mut self) -> Result { 128 | let response = self.session.get_with_redirection(AUTH_SERVER_HOME_URI, 5).await?; 129 | let text = String::from_utf8(response.body().to_vec())?; 130 | let document = Html::parse_document(&text); 131 | 132 | let name: String = document 133 | .select(&Selector::parse("#auth_siderbar > div.auth_username > span > span").unwrap()) 134 | .next() 135 | .map(|e| e.text().collect()) 136 | .unwrap_or_default(); 137 | return Ok(name.trim().to_string()); 138 | } 139 | 140 | async fn get_initial_parameters(&mut self) -> Result { 141 | self.session.clear_cookie(); 142 | 143 | let response = self.session.get(LOGIN_URI).await?; 144 | let text = response.body().to_vec(); 145 | let text = String::from_utf8(text)?; 146 | 147 | fn get_aes_key(text: &str) -> String { 148 | regex_find!(&text, r#"var pwdDefaultEncryptSalt = "(.*?)";"#).unwrap() 149 | } 150 | 151 | fn get_lt_field(text: &str) -> String { 152 | regex_find!(&text, r#""#).unwrap() 153 | } 154 | 155 | let aes_key = get_aes_key(&text); 156 | let lt = get_lt_field(&text); 157 | Ok(IndexParameter { aes_key, lt }) 158 | } 159 | 160 | /// When submit password to `authserver.sit.edu.cn`, it's required to do AES and base64 algorithm with 161 | /// origin password. We use a key from HTML (generated and changed by `JSESSIONID`) to help with. 162 | fn generate_password_string(clear_password: &str, key: &str) -> String { 163 | use base64::engine::general_purpose::STANDARD as base64_standard; 164 | use block_modes::block_padding::Pkcs7; 165 | use block_modes::{BlockMode, Cbc}; 166 | type Aes128Cbc = Cbc; 167 | 168 | // Create an AES object. 169 | let cipher = Aes128Cbc::new_var(key.as_bytes(), &[0u8; 16]).unwrap(); 170 | // Concat plaintext: 64 bytes random bytes and original password. 171 | let mut content = Vec::new(); 172 | content.extend_from_slice(&[0u8; 64]); 173 | content.extend_from_slice(clear_password.as_bytes()); 174 | 175 | // Encrypt with AES and use do base64 encoding. 176 | let encrypted_password = cipher.encrypt_vec(&content); 177 | base64_standard.encode(encrypted_password) 178 | } 179 | 180 | fn parse_err_message(text: &str) -> String { 181 | let document = Html::parse_document(text); 182 | let selector = Selector::parse("#msg").unwrap(); 183 | 184 | document 185 | .select(&selector) 186 | .next() 187 | .map(|e| e.text().collect()) 188 | .unwrap_or_default() 189 | } 190 | 191 | /// Login on campus official auth-server with student id and password. 192 | /// Return session if done successfully. 193 | pub async fn try_login(&mut self) -> Result<()> { 194 | let credential = self.credential.clone(); 195 | let IndexParameter { aes_key, lt } = self.get_initial_parameters().await?; 196 | let encrypted_password = Self::generate_password_string(&credential.password, &aes_key); 197 | 198 | /* Check if captcha is needed. */ 199 | let captcha = if self.check_need_captcha(&credential.account).await? { 200 | let image = self.fetch_captcha().await?; 201 | self.recognize_captcha(image).await? 202 | } else { 203 | String::new() 204 | }; 205 | 206 | /* Send login request */ 207 | let form = vec![ 208 | ("username", credential.account.as_str()), 209 | ("password", &encrypted_password), 210 | ("dllt", "userNamePasswordLogin"), 211 | ("execution", "e1s1"), 212 | ("_eventId", "submit"), 213 | ("rmShown", "1"), 214 | ("captchaResponse", &captcha), 215 | ("lt", <), 216 | ]; 217 | // Login post is the last request. 218 | // Send `Connection: close` to make the server close the connection actively, 219 | // so will the without_shutdown method in the closure in Session::create return. 220 | // Then original stream will be returned. 221 | let header = vec![("Content-Type", "application/x-www-form-urlencoded")]; 222 | let response = self.session.post(LOGIN_URI, form, header).await?; 223 | if response.status() == StatusCode::FOUND { 224 | Ok(()) 225 | } else { 226 | let body = response.body().to_vec(); 227 | let text = String::from_utf8(body)?; 228 | Err(anyhow::anyhow!("{} (from authserver)", Self::parse_err_message(&text))) 229 | } 230 | } 231 | 232 | pub async fn shutdown(mut self) -> Result { 233 | self.session.request_close_connection().await?; 234 | 235 | match self.session.wait_for_shutdown().await { 236 | Ok(mut s) => { 237 | // Close TLS connection (send `TLS Encrypted Alert` message) 238 | s.shutdown().await?; 239 | 240 | // Return original stream back 241 | let (os, _) = s.into_inner(); 242 | Ok(os) 243 | } 244 | Err(e) => Err(e), 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /service-v3/src/authserver/tls.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use std::sync::Arc; 20 | 21 | use once_cell::sync::OnceCell; 22 | use rustls::ClientConfig; 23 | 24 | static TLS_CONFIG: OnceCell> = OnceCell::new(); 25 | 26 | fn generate_tls_config() -> ClientConfig { 27 | fn default_cert_store() -> rustls::RootCertStore { 28 | let mut store = rustls::RootCertStore::empty(); 29 | 30 | store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| { 31 | rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(ta.subject, ta.spki, ta.name_constraints) 32 | })); 33 | store 34 | } 35 | 36 | fn default_client_config() -> ClientConfig { 37 | ClientConfig::builder() 38 | .with_safe_defaults() 39 | .with_root_certificates(default_cert_store()) 40 | .with_no_client_auth() 41 | } 42 | 43 | default_client_config() 44 | } 45 | 46 | pub fn get() -> &'static Arc { 47 | TLS_CONFIG.get_or_init(|| Arc::new(generate_tls_config())) 48 | } 49 | -------------------------------------------------------------------------------- /service-v3/src/error.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use tonic::Status; 20 | 21 | pub trait ToStatus { 22 | fn to_status(self) -> Status; 23 | } 24 | 25 | impl ToStatus for anyhow::Error { 26 | fn to_status(self) -> Status { 27 | Status::internal(self.to_string()) 28 | } 29 | } 30 | 31 | impl ToStatus for sqlx::Error { 32 | fn to_status(self) -> Status { 33 | // TODO: Add log here. 34 | Status::internal("Error occurred in database") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /service-v3/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use kite::config; 20 | use kite::service::KiteModule; 21 | 22 | mod authserver; 23 | mod error; 24 | mod model; 25 | mod service; 26 | 27 | pub struct ServerV3 {} 28 | 29 | #[async_trait::async_trait] 30 | impl KiteModule for ServerV3 { 31 | async fn run() { 32 | service::grpc_server().await 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /service-v3/src/model.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | pub use convert::{ToDateTime, ToTimestamp}; 20 | pub use kite::model::*; 21 | pub use template::*; 22 | 23 | pub mod convert { 24 | use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Timelike}; 25 | use prost_types::Timestamp; 26 | 27 | pub trait ToDateTime { 28 | fn timestamp(self) -> DateTime; 29 | } 30 | 31 | pub trait ToTimestamp { 32 | fn datetime(self) -> Timestamp; 33 | } 34 | 35 | impl ToDateTime for Timestamp { 36 | fn timestamp(self) -> DateTime { 37 | let (secs, nsecs) = (self.seconds, self.nanos); 38 | let dt = NaiveDateTime::from_timestamp_opt(secs, nsecs as u32).unwrap(); 39 | 40 | Local::from_local_datetime(&Local, &dt).unwrap() 41 | } 42 | } 43 | 44 | impl ToTimestamp for DateTime { 45 | fn datetime(self) -> Timestamp { 46 | let (secs, nsecs) = (self.timestamp(), self.nanosecond()); 47 | Timestamp { 48 | seconds: secs, 49 | nanos: nsecs as i32, 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /service-v3/src/service.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use http::request; 20 | use sqlx::PgPool; 21 | use tonic::transport::{Body, Server}; 22 | use tonic_reflection::server::{ServerReflection, ServerReflectionServer}; 23 | 24 | use crate::config; 25 | 26 | pub mod auth; 27 | pub mod gen; 28 | 29 | mod badge; 30 | mod balance; 31 | mod board; 32 | mod captcha; 33 | mod classroom_browser; 34 | mod ping; 35 | mod template; 36 | mod user; 37 | 38 | #[derive(Clone)] 39 | pub struct KiteGrpcServer { 40 | // Postgres instance. 41 | db: PgPool, 42 | } 43 | 44 | /// Used for gRPC reflection. 45 | fn load_reflection() -> ServerReflectionServer { 46 | let file_descriptor = include_bytes!("../../target/compiled-descriptor.bin"); 47 | 48 | tonic_reflection::server::Builder::configure() 49 | .register_encoded_file_descriptor_set(file_descriptor) 50 | .build() 51 | .unwrap() 52 | } 53 | 54 | pub async fn grpc_server() { 55 | let addr = config::get().bind.clone(); 56 | let server = KiteGrpcServer { 57 | db: kite::get_db().clone(), 58 | }; 59 | 60 | let ping = ping::gen::ping_service_server::PingServiceServer::new(server.clone()); 61 | let badge = badge::gen::badge_service_server::BadgeServiceServer::new(server.clone()); 62 | let balance = balance::gen::balance_service_server::BalanceServiceServer::new(server.clone()); 63 | let board = board::gen::board_service_server::BoardServiceServer::new(server.clone()); 64 | let classroom_browser = 65 | classroom_browser::gen::classroom_browser_service_server::ClassroomBrowserServiceServer::new(server.clone()); 66 | let user = user::gen::user_service_server::UserServiceServer::new(server.clone()); 67 | let captcha = captcha::gen::captcha_service_server::CaptchaServiceServer::new(server.clone()); 68 | 69 | use tower_http::trace::TraceLayer; 70 | let layer = tower::ServiceBuilder::new() 71 | .layer( 72 | TraceLayer::new_for_grpc().on_request(|req: &request::Request, _span: &tracing::Span| { 73 | tracing::info!("Incoming request: {:?}", req) 74 | }), 75 | ) 76 | .into_inner(); 77 | 78 | tracing::info!("Listening on {}...", addr); 79 | let builder = Server::builder() 80 | .layer(layer) 81 | .add_service(load_reflection()) 82 | .add_service(ping) 83 | .add_service(badge) 84 | .add_service(balance) 85 | .add_service(board) 86 | .add_service(classroom_browser) 87 | .add_service(user) 88 | .add_service(captcha); 89 | 90 | // Unix socket 91 | let server = if addr.starts_with('/') || addr.starts_with('.') { 92 | #[cfg(not(unix))] 93 | panic!("Unix socket can only be used on Unix-like operating system."); 94 | 95 | #[cfg(unix)] 96 | { 97 | use tokio::net::UnixListener; 98 | use tokio_stream::wrappers::UnixListenerStream; 99 | 100 | let path = addr; 101 | let _ = tokio::fs::remove_file(&path).await; 102 | let uds = UnixListener::bind(&path).expect("Failed to bind unix socket."); 103 | let stream = UnixListenerStream::new(uds); 104 | 105 | builder.serve_with_incoming(stream).await 106 | } 107 | } else { 108 | let addr = addr.parse().unwrap(); 109 | builder.serve(addr).await 110 | }; 111 | 112 | server.unwrap() 113 | } 114 | -------------------------------------------------------------------------------- /service-v3/src/service/auth.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; 20 | 21 | use crate::config; 22 | 23 | #[derive(serde::Serialize, serde::Deserialize)] 24 | pub struct JwtToken { 25 | /// 用户 ID 26 | pub uid: i32, 27 | /// 用户角色 28 | pub role: i32, 29 | } 30 | 31 | impl JwtToken { 32 | pub fn new(uid: i32, role: i32) -> Self { 33 | Self { uid, role } 34 | } 35 | pub fn encode(&self) -> String { 36 | let key = config::get().secret.as_str(); 37 | let encoding_key = EncodingKey::from_secret(key.as_ref()); 38 | 39 | encode(&Header::default(), &self, &encoding_key).unwrap() 40 | } 41 | 42 | pub fn decode(token: &str) -> Option { 43 | let key = config::get().secret.as_str(); 44 | let decoding_key = DecodingKey::from_secret(key.as_ref()); 45 | let option = Validation::default(); 46 | let token_data = decode::(token, &decoding_key, &option); 47 | 48 | token_data.ok().map(|t| t.claims) 49 | } 50 | } 51 | 52 | pub fn get_token_from_request(req: tonic::Request) -> Result { 53 | if let Some(token) = req.metadata().get("authorization") { 54 | let token = token 55 | .to_str() 56 | .map_err(|e| tonic::Status::unauthenticated(format!("Failed to parse token to str: {:?}", e)))?; 57 | 58 | JwtToken::decode(token).ok_or(tonic::Status::unauthenticated("Invalid token: May be expired?")) 59 | } else { 60 | Err(tonic::Status::unauthenticated( 61 | "No authorization can be found in your request.", 62 | )) 63 | } 64 | } 65 | 66 | pub fn require_login(req: tonic::Request) -> Result<(), tonic::Status> { 67 | get_token_from_request(req).map(|_| ()) 68 | } 69 | 70 | pub fn require_user(req: tonic::Request, uid: i32) -> Result<(), tonic::Status> { 71 | match get_token_from_request(req) { 72 | Ok(token) => { 73 | if token.uid == uid { 74 | Ok(()) 75 | } else { 76 | Err(tonic::Status::permission_denied( 77 | "You are manipulating other's resource.", 78 | )) 79 | } 80 | } 81 | Err(e) => Err(tonic::Status::unauthenticated(e.to_string())), 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /service-v3/src/service/badge.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use sqlx::PgPool; 20 | use tonic::{Request, Response, Status}; 21 | 22 | use crate::error::ToStatus; 23 | use crate::model::badge as model; 24 | use crate::service::auth::get_token_from_request; 25 | pub use crate::service::gen::badge as gen; 26 | use crate::service::gen::template::{Empty, EmptyRequest}; 27 | 28 | impl Into for model::Card { 29 | fn into(self) -> gen::Card { 30 | use crate::model::ToTimestamp; 31 | 32 | gen::Card { 33 | card_type: self.card, 34 | ts: Some(ToTimestamp::datetime(self.ts)), 35 | } 36 | } 37 | } 38 | 39 | async fn get_cards_list(pool: &PgPool, uid: i32) -> anyhow::Result> { 40 | let cards = sqlx::query_as("SELECT card, ts FROM new_year_scanning WHERE uid = $1 AND result = 3 AND card != 0;") 41 | .bind(uid) 42 | .fetch_all(pool) 43 | .await? 44 | .into_iter() 45 | .map(|e: model::Card| e.into()) 46 | .collect::>(); 47 | Ok(cards) 48 | } 49 | 50 | async fn append_share_log(pool: &PgPool, uid: i32) -> anyhow::Result<()> { 51 | sqlx::query("INSERT INTO new_year_scanning (uid) VALUES ($1);") 52 | .bind(uid) 53 | .execute(pool) 54 | .await?; 55 | Ok(()) 56 | } 57 | 58 | #[tonic::async_trait] 59 | impl gen::badge_service_server::BadgeService for super::KiteGrpcServer { 60 | async fn get_user_card_storage( 61 | &self, 62 | request: Request, 63 | ) -> Result, Status> { 64 | let token = get_token_from_request(request)?; 65 | let result = get_cards_list(&self.db, token.uid).await.map_err(ToStatus::to_status)?; 66 | 67 | Ok(Response::new(gen::CardListResponse { card_list: result })) 68 | } 69 | 70 | async fn append_share_log(&self, request: Request) -> Result, Status> { 71 | let token = get_token_from_request(request)?; 72 | 73 | append_share_log(&self.db, token.uid) 74 | .await 75 | .map_err(ToStatus::to_status)?; 76 | Ok(Response::new(Empty::default())) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /service-v3/src/service/balance.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use std::ops::Sub; 20 | 21 | use chrono::{DateTime, Duration, Local}; 22 | use tonic::{Request, Response, Status}; 23 | 24 | use kite::model::balance as model; 25 | 26 | use crate::error::ToStatus; 27 | use crate::model::ToTimestamp; 28 | pub use crate::service::gen::balance as gen; 29 | 30 | impl Into for model::ElectricityBalance { 31 | fn into(self) -> gen::RoomBalance { 32 | gen::RoomBalance { 33 | room: self.room, 34 | balance: self.balance, 35 | ts: Some(ToTimestamp::datetime(self.ts)), 36 | } 37 | } 38 | } 39 | 40 | impl Into for model::DailyElectricityBill { 41 | fn into(self) -> gen::BillItem { 42 | use gen::bill_item::Identifier; 43 | 44 | gen::BillItem { 45 | increment: self.charge, 46 | decrement: self.consumption, 47 | identifier: Some(Identifier::Date(self.date)), 48 | } 49 | } 50 | } 51 | 52 | impl Into for model::HourlyElectricityBill { 53 | fn into(self) -> gen::BillItem { 54 | use gen::bill_item::Identifier; 55 | 56 | gen::BillItem { 57 | increment: self.charge, 58 | decrement: self.consumption, 59 | identifier: Some(Identifier::Time(self.time)), 60 | } 61 | } 62 | } 63 | 64 | impl Into for model::RecentConsumptionRank { 65 | fn into(self) -> gen::ConsumptionRank { 66 | gen::ConsumptionRank { 67 | consumption: self.consumption, 68 | rank: self.rank, 69 | total_room: self.room_count, 70 | } 71 | } 72 | } 73 | 74 | #[tonic::async_trait] 75 | impl gen::balance_service_server::BalanceService for super::KiteGrpcServer { 76 | async fn get_room_balance( 77 | &self, 78 | request: Request, 79 | ) -> Result, Status> { 80 | let room = request.into_inner().room_number; 81 | let response = model::get_latest_balance(&self.db, room) 82 | .await 83 | .map_err(ToStatus::to_status)? 84 | .ok_or_else(|| Status::not_found("No such room."))? 85 | .into(); 86 | 87 | Ok(Response::new(response)) 88 | } 89 | 90 | async fn get_consumption_rank( 91 | &self, 92 | request: Request, 93 | ) -> Result, Status> { 94 | let room = request.into_inner().room_number; 95 | let response = model::get_consumption_rank(&self.db, room) 96 | .await 97 | .map_err(ToStatus::to_status)? 98 | .ok_or_else(|| Status::not_found("No such room."))? 99 | .into(); 100 | Ok(Response::new(response)) 101 | } 102 | 103 | async fn get_bill(&self, request: Request) -> Result, Status> { 104 | let request = request.into_inner(); 105 | 106 | // TODO: 107 | // It's defined that the value of gen::BillType::Daily is 0 and that of gen::BillType::Hourly is 1. 108 | // However, according to the reason I don't know till now, tonic (with the compiler prost) sees the "request.type" as i32, 109 | // So I can't use enum match :-( 110 | const IS_DAILY: i32 = 0; 111 | const IS_HOURLY: i32 = 1; 112 | 113 | let bill_list: Vec = match request.r#type { 114 | IS_DAILY => { 115 | let to_str = |x: DateTime| x.format("%Y-%m-%d").to_string(); 116 | 117 | let today = Local::now(); 118 | let last_week = today.sub(Duration::days(7)); 119 | 120 | model::get_bill_in_day(&self.db, request.room_number, to_str(last_week), to_str(today)) 121 | .await 122 | .map_err(ToStatus::to_status)? 123 | .into_iter() 124 | .map(Into::into) 125 | .collect() 126 | } 127 | IS_HOURLY => { 128 | let now = Local::now(); 129 | let yesterday = now.sub(Duration::hours(24)); 130 | 131 | model::get_bill_in_hour(&self.db, request.room_number, yesterday, now) 132 | .await 133 | .map_err(ToStatus::to_status)? 134 | .into_iter() 135 | .map(Into::into) 136 | .collect() 137 | } 138 | _ => { 139 | return Err(Status::invalid_argument("Bill type is unexpected")); 140 | } 141 | }; 142 | Ok(Response::new(gen::BillResponse { bill_list })) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /service-v3/src/service/board.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use sqlx::{PgPool, Postgres}; 20 | use tonic::{Request, Response, Status}; 21 | 22 | use crate::error::ToStatus; 23 | use crate::model::board as model; 24 | use crate::service::board::gen::{Picture, PictureListResponse, UploadRequest}; 25 | pub use crate::service::gen::board as gen; 26 | use crate::service::gen::template::{EmptyRequest, PageOption}; 27 | use crate::service::template::ToPageView; 28 | 29 | impl Into for model::Picture { 30 | fn into(self) -> gen::Picture { 31 | let uuid = super::template::ToUuidMessage::uuid(self.id); 32 | let ts = crate::model::ToTimestamp::datetime(self.ts); 33 | 34 | gen::Picture { 35 | uuid: Some(uuid), 36 | uid: self.uid, 37 | publisher: "".to_string(), // TODO 38 | origin_url: self.url, 39 | thumbnail: self.thumbnail, 40 | ts: Some(ts), 41 | } 42 | } 43 | } 44 | 45 | async fn get_picture_list(pool: &PgPool, page: &crate::model::PageView) -> anyhow::Result> { 46 | sqlx::query_as::( 47 | "SELECT id, uid, path as url, thumbnail, ts, ext FROM picture 48 | WHERE deleted = FALSE 49 | ORDER BY ts DESC 50 | LIMIT $1 OFFSET $2;", 51 | ) 52 | .bind(page.count(20)) 53 | .bind(page.offset(20)) 54 | .fetch_all(pool) 55 | .await 56 | .map(|pic_list| pic_list.into_iter().map(Into::into).collect()) 57 | .map_err(Into::into) 58 | } 59 | 60 | #[tonic::async_trait] 61 | impl gen::board_service_server::BoardService for super::KiteGrpcServer { 62 | async fn get_picture_list(&self, request: Request) -> Result, Status> { 63 | let request = request.into_inner(); 64 | let page = ToPageView::page_option(request); 65 | 66 | get_picture_list(&self.db, &page) 67 | .await 68 | .map(|picture_list| Response::new(PictureListResponse { picture_list })) 69 | .map_err(ToStatus::to_status) 70 | } 71 | 72 | async fn get_my_upload(&self, request: Request) -> Result, Status> { 73 | Err(tonic::Status::unimplemented("todo")) 74 | } 75 | 76 | async fn upload(&self, request: Request) -> Result, Status> { 77 | Err(tonic::Status::unimplemented("todo")) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /service-v3/src/service/captcha.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use tonic::{Request, Response, Status}; 20 | 21 | use crate::error::ToStatus; 22 | use crate::service::captcha::gen::{CaptchaRecognizeRequest, CaptchaRecognizeResponse}; 23 | pub use crate::service::gen::captcha as gen; 24 | 25 | #[tonic::async_trait] 26 | impl gen::captcha_service_server::CaptchaService for super::KiteGrpcServer { 27 | async fn recognize( 28 | &self, 29 | request: Request, 30 | ) -> Result, Status> { 31 | let request = request.into_inner(); 32 | let image = request.image; 33 | 34 | captcha::async_recognize(image) 35 | .await 36 | .map(|text| Response::new(CaptchaRecognizeResponse { text })) 37 | .map_err(ToStatus::to_status) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /service-v3/src/service/classroom_browser.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use sqlx::PgPool; 20 | use tonic::{Request, Response, Status}; 21 | 22 | use crate::error::ToStatus; 23 | use crate::model::classroom_browser as model; 24 | pub use crate::service::gen::classroom_browser as gen; 25 | 26 | impl Into for gen::ClassroomQuery { 27 | fn into(self) -> model::ClassroomQuery { 28 | model::ClassroomQuery { 29 | building: self.building, 30 | region: self.region, 31 | campus: self.campus, 32 | week: self.week, 33 | day: self.day, 34 | want_time: self.time_flag, 35 | } 36 | } 37 | } 38 | 39 | impl Into for model::Classroom { 40 | fn into(self) -> gen::Classroom { 41 | gen::Classroom { 42 | title: self.title, 43 | busy_flag: self.busy_flag, 44 | capacity: self.capacity, 45 | } 46 | } 47 | } 48 | 49 | #[kite::cache_result(timeout = 43200)] 50 | pub async fn query_avail_classroom( 51 | db: &PgPool, 52 | query: &model::ClassroomQuery, 53 | ) -> anyhow::Result> { 54 | sqlx::query_as( 55 | "SELECT room, busy_time::int, capacity::int \ 56 | FROM edu.query_available_classrooms($1, $2, $3, $4, $5, $6);", 57 | ) 58 | .bind(&query.campus) 59 | .bind(&query.building) 60 | .bind(&query.region) 61 | .bind(query.week) 62 | .bind(query.day) 63 | .bind(query.want_time.unwrap_or(!0)) 64 | .fetch_all(db) 65 | .await 66 | .map_err(Into::into) 67 | } 68 | 69 | #[tonic::async_trait] 70 | impl gen::classroom_browser_service_server::ClassroomBrowserService for super::KiteGrpcServer { 71 | async fn get_available_classroom( 72 | &self, 73 | request: Request, 74 | ) -> Result, Status> { 75 | let query = request.into_inner().into(); 76 | 77 | // TODO: Add cache 78 | query_avail_classroom(&self.db, &query) 79 | .await 80 | .map_err(ToStatus::to_status) 81 | .map(|classroom_list| { 82 | let results = classroom_list.into_iter().map(Into::into).collect(); 83 | 84 | Response::new(gen::ClassroomListResponse { 85 | classroom_list: results, 86 | }) 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /service-v3/src/service/gen.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | pub mod badge; 20 | pub mod balance; 21 | pub mod board; 22 | pub mod captcha; 23 | pub mod classroom_browser; 24 | pub mod exception; 25 | pub mod freshman; 26 | pub mod game; 27 | pub mod ping; 28 | pub mod template; 29 | pub mod token; 30 | pub mod typing; 31 | pub mod user; 32 | pub mod yellow_page; 33 | -------------------------------------------------------------------------------- /service-v3/src/service/gen/badge.rs: -------------------------------------------------------------------------------- 1 | /// 用户 “扫福” 记录 2 | #[allow(clippy::derive_partial_eq_without_eq)] 3 | #[derive(Clone, PartialEq, ::prost::Message)] 4 | pub struct ScanRecord { 5 | /// 用户 ID 6 | /// int32 uid = 1; 7 | /// “扫福” 结果类型 8 | #[prost(enumeration = "ScanResult", tag = "2")] 9 | pub r#type: i32, 10 | /// 抽到的卡类型。暂且考虑到扩展性,使用 int 类型表示 11 | #[prost(int32, optional, tag = "3")] 12 | pub card: ::core::option::Option, 13 | /// 触发的时间 14 | #[prost(message, optional, tag = "4")] 15 | pub ts: ::core::option::Option<::prost_types::Timestamp>, 16 | } 17 | /// 用户卡片 18 | #[allow(clippy::derive_partial_eq_without_eq)] 19 | #[derive(Clone, PartialEq, ::prost::Message)] 20 | pub struct Card { 21 | /// 卡片类型 22 | #[prost(int32, tag = "1")] 23 | pub card_type: i32, 24 | /// 抽卡时间 25 | #[prost(message, optional, tag = "2")] 26 | pub ts: ::core::option::Option<::prost_types::Timestamp>, 27 | } 28 | #[allow(clippy::derive_partial_eq_without_eq)] 29 | #[derive(Clone, PartialEq, ::prost::Message)] 30 | pub struct CardListResponse { 31 | #[prost(message, repeated, tag = "1")] 32 | pub card_list: ::prost::alloc::vec::Vec, 33 | } 34 | /// 用户 “扫福” 结果 35 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] 36 | #[repr(i32)] 37 | pub enum ScanResult { 38 | /// 没有识别到校徽 39 | NoBadge = 0, 40 | /// 当日领福卡次数已达到限制 41 | ReachLimit = 1, 42 | /// 没有抽中 43 | NoCard = 2, 44 | /// 抽中了 45 | WinCard = 3, 46 | } 47 | impl ScanResult { 48 | /// String value of the enum field names used in the ProtoBuf definition. 49 | /// 50 | /// The values are not transformed in any way and thus are considered stable 51 | /// (if the ProtoBuf definition does not change) and safe for programmatic use. 52 | pub fn as_str_name(&self) -> &'static str { 53 | match self { 54 | ScanResult::NoBadge => "NoBadge", 55 | ScanResult::ReachLimit => "ReachLimit", 56 | ScanResult::NoCard => "NoCard", 57 | ScanResult::WinCard => "WinCard", 58 | } 59 | } 60 | /// Creates an enum from field names used in the ProtoBuf definition. 61 | pub fn from_str_name(value: &str) -> ::core::option::Option { 62 | match value { 63 | "NoBadge" => Some(Self::NoBadge), 64 | "ReachLimit" => Some(Self::ReachLimit), 65 | "NoCard" => Some(Self::NoCard), 66 | "WinCard" => Some(Self::WinCard), 67 | _ => None, 68 | } 69 | } 70 | } 71 | /// Generated server implementations. 72 | pub mod badge_service_server { 73 | #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] 74 | 75 | use tonic::codegen::*; 76 | 77 | /// Generated trait containing gRPC methods that should be implemented for use with BadgeServiceServer. 78 | #[async_trait] 79 | pub trait BadgeService: Send + Sync + 'static { 80 | /// 获取用户所抽到的所有卡片 81 | async fn get_user_card_storage( 82 | &self, 83 | request: tonic::Request, 84 | ) -> Result, tonic::Status>; 85 | /// 记录用户分享事件 86 | /// 该方法用于增加用户抽卡次数(2022春节) 87 | async fn append_share_log( 88 | &self, 89 | request: tonic::Request, 90 | ) -> Result, tonic::Status>; 91 | } 92 | #[derive(Debug)] 93 | pub struct BadgeServiceServer { 94 | inner: _Inner, 95 | accept_compression_encodings: EnabledCompressionEncodings, 96 | send_compression_encodings: EnabledCompressionEncodings, 97 | } 98 | struct _Inner(Arc); 99 | impl BadgeServiceServer { 100 | pub fn new(inner: T) -> Self { 101 | Self::from_arc(Arc::new(inner)) 102 | } 103 | pub fn from_arc(inner: Arc) -> Self { 104 | let inner = _Inner(inner); 105 | Self { 106 | inner, 107 | accept_compression_encodings: Default::default(), 108 | send_compression_encodings: Default::default(), 109 | } 110 | } 111 | pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService 112 | where 113 | F: tonic::service::Interceptor, 114 | { 115 | InterceptedService::new(Self::new(inner), interceptor) 116 | } 117 | /// Enable decompressing requests with the given encoding. 118 | #[must_use] 119 | pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 120 | self.accept_compression_encodings.enable(encoding); 121 | self 122 | } 123 | /// Compress responses with the given encoding, if the client supports it. 124 | #[must_use] 125 | pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 126 | self.send_compression_encodings.enable(encoding); 127 | self 128 | } 129 | } 130 | impl tonic::codegen::Service> for BadgeServiceServer 131 | where 132 | T: BadgeService, 133 | B: Body + Send + 'static, 134 | B::Error: Into + Send + 'static, 135 | { 136 | type Response = http::Response; 137 | type Error = std::convert::Infallible; 138 | type Future = BoxFuture; 139 | fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { 140 | Poll::Ready(Ok(())) 141 | } 142 | fn call(&mut self, req: http::Request) -> Self::Future { 143 | let inner = self.inner.clone(); 144 | match req.uri().path() { 145 | "/badge.BadgeService/GetUserCardStorage" => { 146 | #[allow(non_camel_case_types)] 147 | struct GetUserCardStorageSvc(pub Arc); 148 | impl tonic::server::UnaryService for GetUserCardStorageSvc { 149 | type Response = super::CardListResponse; 150 | type Future = BoxFuture, tonic::Status>; 151 | fn call( 152 | &mut self, 153 | request: tonic::Request, 154 | ) -> Self::Future { 155 | let inner = self.0.clone(); 156 | let fut = async move { (*inner).get_user_card_storage(request).await }; 157 | Box::pin(fut) 158 | } 159 | } 160 | let accept_compression_encodings = self.accept_compression_encodings; 161 | let send_compression_encodings = self.send_compression_encodings; 162 | let inner = self.inner.clone(); 163 | let fut = async move { 164 | let inner = inner.0; 165 | let method = GetUserCardStorageSvc(inner); 166 | let codec = tonic::codec::ProstCodec::default(); 167 | let mut grpc = tonic::server::Grpc::new(codec) 168 | .apply_compression_config(accept_compression_encodings, send_compression_encodings); 169 | let res = grpc.unary(method, req).await; 170 | Ok(res) 171 | }; 172 | Box::pin(fut) 173 | } 174 | "/badge.BadgeService/AppendShareLog" => { 175 | #[allow(non_camel_case_types)] 176 | struct AppendShareLogSvc(pub Arc); 177 | impl tonic::server::UnaryService for AppendShareLogSvc { 178 | type Response = super::super::template::Empty; 179 | type Future = BoxFuture, tonic::Status>; 180 | fn call( 181 | &mut self, 182 | request: tonic::Request, 183 | ) -> Self::Future { 184 | let inner = self.0.clone(); 185 | let fut = async move { (*inner).append_share_log(request).await }; 186 | Box::pin(fut) 187 | } 188 | } 189 | let accept_compression_encodings = self.accept_compression_encodings; 190 | let send_compression_encodings = self.send_compression_encodings; 191 | let inner = self.inner.clone(); 192 | let fut = async move { 193 | let inner = inner.0; 194 | let method = AppendShareLogSvc(inner); 195 | let codec = tonic::codec::ProstCodec::default(); 196 | let mut grpc = tonic::server::Grpc::new(codec) 197 | .apply_compression_config(accept_compression_encodings, send_compression_encodings); 198 | let res = grpc.unary(method, req).await; 199 | Ok(res) 200 | }; 201 | Box::pin(fut) 202 | } 203 | _ => Box::pin(async move { 204 | Ok(http::Response::builder() 205 | .status(200) 206 | .header("grpc-status", "12") 207 | .header("content-type", "application/grpc") 208 | .body(empty_body()) 209 | .unwrap()) 210 | }), 211 | } 212 | } 213 | } 214 | impl Clone for BadgeServiceServer { 215 | fn clone(&self) -> Self { 216 | let inner = self.inner.clone(); 217 | Self { 218 | inner, 219 | accept_compression_encodings: self.accept_compression_encodings, 220 | send_compression_encodings: self.send_compression_encodings, 221 | } 222 | } 223 | } 224 | impl Clone for _Inner { 225 | fn clone(&self) -> Self { 226 | Self(self.0.clone()) 227 | } 228 | } 229 | impl std::fmt::Debug for _Inner { 230 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 231 | write!(f, "{:?}", self.0) 232 | } 233 | } 234 | impl tonic::server::NamedService for BadgeServiceServer { 235 | const NAME: &'static str = "badge.BadgeService"; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /service-v3/src/service/gen/captcha.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #[allow(clippy::derive_partial_eq_without_eq)] 20 | #[derive(Clone, PartialEq, ::prost::Message)] 21 | pub struct CaptchaRecognizeRequest { 22 | #[prost(bytes = "vec", tag = "1")] 23 | pub image: ::prost::alloc::vec::Vec, 24 | } 25 | #[allow(clippy::derive_partial_eq_without_eq)] 26 | #[derive(Clone, PartialEq, ::prost::Message)] 27 | pub struct CaptchaRecognizeResponse { 28 | #[prost(string, tag = "1")] 29 | pub text: ::prost::alloc::string::String, 30 | } 31 | /// Generated server implementations. 32 | pub mod captcha_service_server { 33 | #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] 34 | 35 | use tonic::codegen::*; 36 | 37 | /// Generated trait containing gRPC methods that should be implemented for use with CaptchaServiceServer. 38 | #[async_trait] 39 | pub trait CaptchaService: Send + Sync + 'static { 40 | async fn recognize( 41 | &self, 42 | request: tonic::Request, 43 | ) -> Result, tonic::Status>; 44 | } 45 | #[derive(Debug)] 46 | pub struct CaptchaServiceServer { 47 | inner: _Inner, 48 | accept_compression_encodings: EnabledCompressionEncodings, 49 | send_compression_encodings: EnabledCompressionEncodings, 50 | } 51 | struct _Inner(Arc); 52 | impl CaptchaServiceServer { 53 | pub fn new(inner: T) -> Self { 54 | Self::from_arc(Arc::new(inner)) 55 | } 56 | pub fn from_arc(inner: Arc) -> Self { 57 | let inner = _Inner(inner); 58 | Self { 59 | inner, 60 | accept_compression_encodings: Default::default(), 61 | send_compression_encodings: Default::default(), 62 | } 63 | } 64 | pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService 65 | where 66 | F: tonic::service::Interceptor, 67 | { 68 | InterceptedService::new(Self::new(inner), interceptor) 69 | } 70 | /// Enable decompressing requests with the given encoding. 71 | #[must_use] 72 | pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 73 | self.accept_compression_encodings.enable(encoding); 74 | self 75 | } 76 | /// Compress responses with the given encoding, if the client supports it. 77 | #[must_use] 78 | pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 79 | self.send_compression_encodings.enable(encoding); 80 | self 81 | } 82 | } 83 | impl tonic::codegen::Service> for CaptchaServiceServer 84 | where 85 | T: CaptchaService, 86 | B: Body + Send + 'static, 87 | B::Error: Into + Send + 'static, 88 | { 89 | type Response = http::Response; 90 | type Error = std::convert::Infallible; 91 | type Future = BoxFuture; 92 | fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { 93 | Poll::Ready(Ok(())) 94 | } 95 | fn call(&mut self, req: http::Request) -> Self::Future { 96 | let inner = self.inner.clone(); 97 | match req.uri().path() { 98 | "/captcha.CaptchaService/Recognize" => { 99 | #[allow(non_camel_case_types)] 100 | struct RecognizeSvc(pub Arc); 101 | impl tonic::server::UnaryService for RecognizeSvc { 102 | type Response = super::CaptchaRecognizeResponse; 103 | type Future = BoxFuture, tonic::Status>; 104 | fn call(&mut self, request: tonic::Request) -> Self::Future { 105 | let inner = self.0.clone(); 106 | let fut = async move { (*inner).recognize(request).await }; 107 | Box::pin(fut) 108 | } 109 | } 110 | let accept_compression_encodings = self.accept_compression_encodings; 111 | let send_compression_encodings = self.send_compression_encodings; 112 | let inner = self.inner.clone(); 113 | let fut = async move { 114 | let inner = inner.0; 115 | let method = RecognizeSvc(inner); 116 | let codec = tonic::codec::ProstCodec::default(); 117 | let mut grpc = tonic::server::Grpc::new(codec) 118 | .apply_compression_config(accept_compression_encodings, send_compression_encodings); 119 | let res = grpc.unary(method, req).await; 120 | Ok(res) 121 | }; 122 | Box::pin(fut) 123 | } 124 | _ => Box::pin(async move { 125 | Ok(http::Response::builder() 126 | .status(200) 127 | .header("grpc-status", "12") 128 | .header("content-type", "application/grpc") 129 | .body(empty_body()) 130 | .unwrap()) 131 | }), 132 | } 133 | } 134 | } 135 | impl Clone for CaptchaServiceServer { 136 | fn clone(&self) -> Self { 137 | let inner = self.inner.clone(); 138 | Self { 139 | inner, 140 | accept_compression_encodings: self.accept_compression_encodings, 141 | send_compression_encodings: self.send_compression_encodings, 142 | } 143 | } 144 | } 145 | impl Clone for _Inner { 146 | fn clone(&self) -> Self { 147 | Self(self.0.clone()) 148 | } 149 | } 150 | impl std::fmt::Debug for _Inner { 151 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 152 | write!(f, "{:?}", self.0) 153 | } 154 | } 155 | impl tonic::server::NamedService for CaptchaServiceServer { 156 | const NAME: &'static str = "captcha.CaptchaService"; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /service-v3/src/service/gen/classroom_browser.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #[allow(clippy::derive_partial_eq_without_eq)] 20 | #[derive(Clone, PartialEq, ::prost::Message)] 21 | pub struct ClassroomQuery { 22 | /// 教学楼名称,如 "一教" 23 | #[prost(string, optional, tag = "1")] 24 | pub building: ::core::option::Option<::prost::alloc::string::String>, 25 | /// 教学区域名称,如 "A", "B" 26 | #[prost(string, optional, tag = "2")] 27 | pub region: ::core::option::Option<::prost::alloc::string::String>, 28 | /// 校区 29 | #[prost(enumeration = "super::typing::Campus", optional, tag = "3")] 30 | pub campus: ::core::option::Option, 31 | /// 当前学期的周序号,一般为 1-18 32 | #[prost(int32, tag = "4")] 33 | pub week: i32, 34 | /// 星期几,取值 1 - 7 35 | #[prost(int32, tag = "5")] 36 | pub day: i32, 37 | /// 期望有空闲的时间,使用二进制位表示。 38 | /// 如果某一位(从右,从 0 开始计数)为 1,比如从右数第 1 位为 1, 那么表示希望第一节课空闲 39 | /// 40 | /// 值 110b 表示希望 1-2 节课,即请求 8:20-9:55 空闲的教室,如果当前值省略,默认不筛选时间。 41 | #[prost(int32, optional, tag = "6")] 42 | pub time_flag: ::core::option::Option, 43 | } 44 | /// 教室信息 45 | #[allow(clippy::derive_partial_eq_without_eq)] 46 | #[derive(Clone, PartialEq, ::prost::Message)] 47 | pub struct Classroom { 48 | /// 教室名称,如 "C103" 49 | #[prost(string, tag = "1")] 50 | pub title: ::prost::alloc::string::String, 51 | /// 教室使用情况,同 time_flag, 是一个用位表示的标记 52 | #[prost(int32, tag = "2")] 53 | pub busy_flag: i32, 54 | /// 教室容量,部分教室暂缺 55 | #[prost(int32, optional, tag = "3")] 56 | pub capacity: ::core::option::Option, 57 | } 58 | #[allow(clippy::derive_partial_eq_without_eq)] 59 | #[derive(Clone, PartialEq, ::prost::Message)] 60 | pub struct ClassroomListResponse { 61 | #[prost(message, repeated, tag = "1")] 62 | pub classroom_list: ::prost::alloc::vec::Vec, 63 | } 64 | /// Generated server implementations. 65 | pub mod classroom_browser_service_server { 66 | #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] 67 | 68 | use tonic::codegen::*; 69 | 70 | /// Generated trait containing gRPC methods that should be implemented for use with ClassroomBrowserServiceServer. 71 | #[async_trait] 72 | pub trait ClassroomBrowserService: Send + Sync + 'static { 73 | /// 根据给定位置和时间,获取空教室列表 74 | async fn get_available_classroom( 75 | &self, 76 | request: tonic::Request, 77 | ) -> Result, tonic::Status>; 78 | } 79 | #[derive(Debug)] 80 | pub struct ClassroomBrowserServiceServer { 81 | inner: _Inner, 82 | accept_compression_encodings: EnabledCompressionEncodings, 83 | send_compression_encodings: EnabledCompressionEncodings, 84 | } 85 | struct _Inner(Arc); 86 | impl ClassroomBrowserServiceServer { 87 | pub fn new(inner: T) -> Self { 88 | Self::from_arc(Arc::new(inner)) 89 | } 90 | pub fn from_arc(inner: Arc) -> Self { 91 | let inner = _Inner(inner); 92 | Self { 93 | inner, 94 | accept_compression_encodings: Default::default(), 95 | send_compression_encodings: Default::default(), 96 | } 97 | } 98 | pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService 99 | where 100 | F: tonic::service::Interceptor, 101 | { 102 | InterceptedService::new(Self::new(inner), interceptor) 103 | } 104 | /// Enable decompressing requests with the given encoding. 105 | #[must_use] 106 | pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 107 | self.accept_compression_encodings.enable(encoding); 108 | self 109 | } 110 | /// Compress responses with the given encoding, if the client supports it. 111 | #[must_use] 112 | pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 113 | self.send_compression_encodings.enable(encoding); 114 | self 115 | } 116 | } 117 | 118 | impl tonic::codegen::Service> for ClassroomBrowserServiceServer 119 | where 120 | T: ClassroomBrowserService, 121 | B: Body + Send + 'static, 122 | B::Error: Into + Send + 'static, 123 | { 124 | type Response = http::Response; 125 | type Error = std::convert::Infallible; 126 | type Future = BoxFuture; 127 | fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { 128 | Poll::Ready(Ok(())) 129 | } 130 | fn call(&mut self, req: http::Request) -> Self::Future { 131 | let inner = self.inner.clone(); 132 | match req.uri().path() { 133 | "/classroom_browser.ClassroomBrowserService/GetAvailableClassroom" => { 134 | #[allow(non_camel_case_types)] 135 | struct GetAvailableClassroomSvc(pub Arc); 136 | impl tonic::server::UnaryService for GetAvailableClassroomSvc { 137 | type Response = super::ClassroomListResponse; 138 | type Future = BoxFuture, tonic::Status>; 139 | fn call(&mut self, request: tonic::Request) -> Self::Future { 140 | let inner = self.0.clone(); 141 | let fut = async move { (*inner).get_available_classroom(request).await }; 142 | Box::pin(fut) 143 | } 144 | } 145 | let accept_compression_encodings = self.accept_compression_encodings; 146 | let send_compression_encodings = self.send_compression_encodings; 147 | let inner = self.inner.clone(); 148 | let fut = async move { 149 | let inner = inner.0; 150 | let method = GetAvailableClassroomSvc(inner); 151 | let codec = tonic::codec::ProstCodec::default(); 152 | let mut grpc = tonic::server::Grpc::new(codec) 153 | .apply_compression_config(accept_compression_encodings, send_compression_encodings); 154 | let res = grpc.unary(method, req).await; 155 | Ok(res) 156 | }; 157 | Box::pin(fut) 158 | } 159 | _ => Box::pin(async move { 160 | Ok(http::Response::builder() 161 | .status(200) 162 | .header("grpc-status", "12") 163 | .header("content-type", "application/grpc") 164 | .body(empty_body()) 165 | .unwrap()) 166 | }), 167 | } 168 | } 169 | } 170 | impl Clone for ClassroomBrowserServiceServer { 171 | fn clone(&self) -> Self { 172 | let inner = self.inner.clone(); 173 | Self { 174 | inner, 175 | accept_compression_encodings: self.accept_compression_encodings, 176 | send_compression_encodings: self.send_compression_encodings, 177 | } 178 | } 179 | } 180 | 181 | impl Clone for _Inner { 182 | fn clone(&self) -> Self { 183 | Self(self.0.clone()) 184 | } 185 | } 186 | 187 | impl std::fmt::Debug for _Inner { 188 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 189 | write!(f, "{:?}", self.0) 190 | } 191 | } 192 | 193 | impl tonic::server::NamedService for ClassroomBrowserServiceServer { 194 | const NAME: &'static str = "classroom_browser.ClassroomBrowserService"; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /service-v3/src/service/gen/exception.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | /// 上报的异常信息,由前端 app 自动生成 20 | #[allow(clippy::derive_partial_eq_without_eq)] 21 | #[derive(Clone, PartialEq, ::prost::Message)] 22 | pub struct Exception { 23 | /// 错误基本描述 24 | #[prost(string, tag = "1")] 25 | pub error: ::prost::alloc::string::String, 26 | /// 错误发生的时间(用户本地) 27 | #[prost(message, optional, tag = "2")] 28 | pub ts: ::core::option::Option<::prost_types::Timestamp>, 29 | /// 调用栈 30 | #[prost(string, tag = "3")] 31 | pub stack: ::prost::alloc::string::String, 32 | /// 用户平台,JSON 33 | /// 注意校验其结构 34 | #[prost(string, tag = "4")] 35 | pub platform: ::prost::alloc::string::String, 36 | /// 其他,JSON 37 | #[prost(string, tag = "5")] 38 | pub custom: ::prost::alloc::string::String, 39 | /// 设备信息, JSON 40 | #[prost(string, tag = "6")] 41 | pub device: ::prost::alloc::string::String, 42 | /// 程序版本信息等,JSON 43 | #[prost(string, tag = "7")] 44 | pub application: ::prost::alloc::string::String, 45 | } 46 | /// Generated server implementations. 47 | pub mod exception_service_server { 48 | #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] 49 | 50 | use tonic::codegen::*; 51 | 52 | /// Generated trait containing gRPC methods that should be implemented for use with ExceptionServiceServer. 53 | #[async_trait] 54 | pub trait ExceptionService: Send + Sync + 'static { 55 | /// 上报异常信息 56 | async fn report_exception( 57 | &self, 58 | request: tonic::Request, 59 | ) -> Result, tonic::Status>; 60 | } 61 | #[derive(Debug)] 62 | pub struct ExceptionServiceServer { 63 | inner: _Inner, 64 | accept_compression_encodings: EnabledCompressionEncodings, 65 | send_compression_encodings: EnabledCompressionEncodings, 66 | } 67 | struct _Inner(Arc); 68 | impl ExceptionServiceServer { 69 | pub fn new(inner: T) -> Self { 70 | Self::from_arc(Arc::new(inner)) 71 | } 72 | pub fn from_arc(inner: Arc) -> Self { 73 | let inner = _Inner(inner); 74 | Self { 75 | inner, 76 | accept_compression_encodings: Default::default(), 77 | send_compression_encodings: Default::default(), 78 | } 79 | } 80 | pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService 81 | where 82 | F: tonic::service::Interceptor, 83 | { 84 | InterceptedService::new(Self::new(inner), interceptor) 85 | } 86 | /// Enable decompressing requests with the given encoding. 87 | #[must_use] 88 | pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 89 | self.accept_compression_encodings.enable(encoding); 90 | self 91 | } 92 | /// Compress responses with the given encoding, if the client supports it. 93 | #[must_use] 94 | pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 95 | self.send_compression_encodings.enable(encoding); 96 | self 97 | } 98 | } 99 | impl tonic::codegen::Service> for ExceptionServiceServer 100 | where 101 | T: ExceptionService, 102 | B: Body + Send + 'static, 103 | B::Error: Into + Send + 'static, 104 | { 105 | type Response = http::Response; 106 | type Error = std::convert::Infallible; 107 | type Future = BoxFuture; 108 | fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { 109 | Poll::Ready(Ok(())) 110 | } 111 | fn call(&mut self, req: http::Request) -> Self::Future { 112 | let inner = self.inner.clone(); 113 | match req.uri().path() { 114 | "/exception.ExceptionService/ReportException" => { 115 | #[allow(non_camel_case_types)] 116 | struct ReportExceptionSvc(pub Arc); 117 | impl tonic::server::UnaryService for ReportExceptionSvc { 118 | type Response = super::super::template::Empty; 119 | type Future = BoxFuture, tonic::Status>; 120 | fn call(&mut self, request: tonic::Request) -> Self::Future { 121 | let inner = self.0.clone(); 122 | let fut = async move { (*inner).report_exception(request).await }; 123 | Box::pin(fut) 124 | } 125 | } 126 | let accept_compression_encodings = self.accept_compression_encodings; 127 | let send_compression_encodings = self.send_compression_encodings; 128 | let inner = self.inner.clone(); 129 | let fut = async move { 130 | let inner = inner.0; 131 | let method = ReportExceptionSvc(inner); 132 | let codec = tonic::codec::ProstCodec::default(); 133 | let mut grpc = tonic::server::Grpc::new(codec) 134 | .apply_compression_config(accept_compression_encodings, send_compression_encodings); 135 | let res = grpc.unary(method, req).await; 136 | Ok(res) 137 | }; 138 | Box::pin(fut) 139 | } 140 | _ => Box::pin(async move { 141 | Ok(http::Response::builder() 142 | .status(200) 143 | .header("grpc-status", "12") 144 | .header("content-type", "application/grpc") 145 | .body(empty_body()) 146 | .unwrap()) 147 | }), 148 | } 149 | } 150 | } 151 | impl Clone for ExceptionServiceServer { 152 | fn clone(&self) -> Self { 153 | let inner = self.inner.clone(); 154 | Self { 155 | inner, 156 | accept_compression_encodings: self.accept_compression_encodings, 157 | send_compression_encodings: self.send_compression_encodings, 158 | } 159 | } 160 | } 161 | impl Clone for _Inner { 162 | fn clone(&self) -> Self { 163 | Self(self.0.clone()) 164 | } 165 | } 166 | impl std::fmt::Debug for _Inner { 167 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 168 | write!(f, "{:?}", self.0) 169 | } 170 | } 171 | impl tonic::server::NamedService for ExceptionServiceServer { 172 | const NAME: &'static str = "exception.ExceptionService"; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /service-v3/src/service/gen/ping.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #[allow(clippy::derive_partial_eq_without_eq)] 20 | #[derive(Clone, PartialEq, ::prost::Message)] 21 | pub struct PingRequest { 22 | #[prost(string, tag = "1")] 23 | pub text: ::prost::alloc::string::String, 24 | } 25 | #[allow(clippy::derive_partial_eq_without_eq)] 26 | #[derive(Clone, PartialEq, ::prost::Message)] 27 | pub struct PongResponse { 28 | #[prost(string, tag = "1")] 29 | pub text: ::prost::alloc::string::String, 30 | } 31 | /// Generated server implementations. 32 | pub mod ping_service_server { 33 | #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] 34 | 35 | use tonic::codegen::*; 36 | 37 | /// Generated trait containing gRPC methods that should be implemented for use with PingServiceServer. 38 | #[async_trait] 39 | pub trait PingService: Send + Sync + 'static { 40 | /// 发送测试用 ping 请求 41 | async fn ping( 42 | &self, 43 | request: tonic::Request, 44 | ) -> Result, tonic::Status>; 45 | } 46 | #[derive(Debug)] 47 | pub struct PingServiceServer { 48 | inner: _Inner, 49 | accept_compression_encodings: EnabledCompressionEncodings, 50 | send_compression_encodings: EnabledCompressionEncodings, 51 | } 52 | struct _Inner(Arc); 53 | impl PingServiceServer { 54 | pub fn new(inner: T) -> Self { 55 | Self::from_arc(Arc::new(inner)) 56 | } 57 | pub fn from_arc(inner: Arc) -> Self { 58 | let inner = _Inner(inner); 59 | Self { 60 | inner, 61 | accept_compression_encodings: Default::default(), 62 | send_compression_encodings: Default::default(), 63 | } 64 | } 65 | pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService 66 | where 67 | F: tonic::service::Interceptor, 68 | { 69 | InterceptedService::new(Self::new(inner), interceptor) 70 | } 71 | /// Enable decompressing requests with the given encoding. 72 | #[must_use] 73 | pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 74 | self.accept_compression_encodings.enable(encoding); 75 | self 76 | } 77 | /// Compress responses with the given encoding, if the client supports it. 78 | #[must_use] 79 | pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 80 | self.send_compression_encodings.enable(encoding); 81 | self 82 | } 83 | } 84 | impl tonic::codegen::Service> for PingServiceServer 85 | where 86 | T: PingService, 87 | B: Body + Send + 'static, 88 | B::Error: Into + Send + 'static, 89 | { 90 | type Response = http::Response; 91 | type Error = std::convert::Infallible; 92 | type Future = BoxFuture; 93 | fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { 94 | Poll::Ready(Ok(())) 95 | } 96 | fn call(&mut self, req: http::Request) -> Self::Future { 97 | let inner = self.inner.clone(); 98 | match req.uri().path() { 99 | "/ping.PingService/Ping" => { 100 | #[allow(non_camel_case_types)] 101 | struct PingSvc(pub Arc); 102 | impl tonic::server::UnaryService for PingSvc { 103 | type Response = super::PongResponse; 104 | type Future = BoxFuture, tonic::Status>; 105 | fn call(&mut self, request: tonic::Request) -> Self::Future { 106 | let inner = self.0.clone(); 107 | let fut = async move { (*inner).ping(request).await }; 108 | Box::pin(fut) 109 | } 110 | } 111 | let accept_compression_encodings = self.accept_compression_encodings; 112 | let send_compression_encodings = self.send_compression_encodings; 113 | let inner = self.inner.clone(); 114 | let fut = async move { 115 | let inner = inner.0; 116 | let method = PingSvc(inner); 117 | let codec = tonic::codec::ProstCodec::default(); 118 | let mut grpc = tonic::server::Grpc::new(codec) 119 | .apply_compression_config(accept_compression_encodings, send_compression_encodings); 120 | let res = grpc.unary(method, req).await; 121 | Ok(res) 122 | }; 123 | Box::pin(fut) 124 | } 125 | _ => Box::pin(async move { 126 | Ok(http::Response::builder() 127 | .status(200) 128 | .header("grpc-status", "12") 129 | .header("content-type", "application/grpc") 130 | .body(empty_body()) 131 | .unwrap()) 132 | }), 133 | } 134 | } 135 | } 136 | impl Clone for PingServiceServer { 137 | fn clone(&self) -> Self { 138 | let inner = self.inner.clone(); 139 | Self { 140 | inner, 141 | accept_compression_encodings: self.accept_compression_encodings, 142 | send_compression_encodings: self.send_compression_encodings, 143 | } 144 | } 145 | } 146 | impl Clone for _Inner { 147 | fn clone(&self) -> Self { 148 | Self(self.0.clone()) 149 | } 150 | } 151 | impl std::fmt::Debug for _Inner { 152 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 153 | write!(f, "{:?}", self.0) 154 | } 155 | } 156 | impl tonic::server::NamedService for PingServiceServer { 157 | const NAME: &'static str = "ping.PingService"; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /service-v3/src/service/gen/template.rs: -------------------------------------------------------------------------------- 1 | /// Empty message 2 | #[allow(clippy::derive_partial_eq_without_eq)] 3 | #[derive(Clone, PartialEq, ::prost::Message)] 4 | pub struct Empty {} 5 | /// Empty request 6 | #[allow(clippy::derive_partial_eq_without_eq)] 7 | #[derive(Clone, PartialEq, ::prost::Message)] 8 | pub struct EmptyRequest {} 9 | /// Page options 10 | #[allow(clippy::derive_partial_eq_without_eq)] 11 | #[derive(Clone, PartialEq, ::prost::Message)] 12 | pub struct PageOption { 13 | #[prost(int32, tag = "1")] 14 | pub size: i32, 15 | #[prost(int32, tag = "2")] 16 | pub index: i32, 17 | #[prost(enumeration = "PageSort", optional, tag = "3")] 18 | pub sort: ::core::option::Option, 19 | } 20 | /// UUID type 21 | #[allow(clippy::derive_partial_eq_without_eq)] 22 | #[derive(Clone, PartialEq, ::prost::Message)] 23 | pub struct Uuid { 24 | #[prost(string, tag = "1")] 25 | pub value: ::prost::alloc::string::String, 26 | } 27 | /// Page sort method 28 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] 29 | #[repr(i32)] 30 | pub enum PageSort { 31 | Asc = 0, 32 | Desc = 1, 33 | } 34 | impl PageSort { 35 | /// String value of the enum field names used in the ProtoBuf definition. 36 | /// 37 | /// The values are not transformed in any way and thus are considered stable 38 | /// (if the ProtoBuf definition does not change) and safe for programmatic use. 39 | pub fn as_str_name(&self) -> &'static str { 40 | match self { 41 | PageSort::Asc => "Asc", 42 | PageSort::Desc => "Desc", 43 | } 44 | } 45 | /// Creates an enum from field names used in the ProtoBuf definition. 46 | pub fn from_str_name(value: &str) -> ::core::option::Option { 47 | match value { 48 | "Asc" => Some(Self::Asc), 49 | "Desc" => Some(Self::Desc), 50 | _ => None, 51 | } 52 | } 53 | } 54 | /// 性别 55 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] 56 | #[repr(i32)] 57 | pub enum Gender { 58 | Male = 0, 59 | Female = 1, 60 | } 61 | impl Gender { 62 | /// String value of the enum field names used in the ProtoBuf definition. 63 | /// 64 | /// The values are not transformed in any way and thus are considered stable 65 | /// (if the ProtoBuf definition does not change) and safe for programmatic use. 66 | pub fn as_str_name(&self) -> &'static str { 67 | match self { 68 | Gender::Male => "Male", 69 | Gender::Female => "Female", 70 | } 71 | } 72 | /// Creates an enum from field names used in the ProtoBuf definition. 73 | pub fn from_str_name(value: &str) -> ::core::option::Option { 74 | match value { 75 | "Male" => Some(Self::Male), 76 | "Female" => Some(Self::Female), 77 | _ => None, 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /service-v3/src/service/gen/token.rs: -------------------------------------------------------------------------------- 1 | /// 用户访问令牌 2 | #[allow(clippy::derive_partial_eq_without_eq)] 3 | #[derive(Clone, PartialEq, ::prost::Message)] 4 | pub struct UserToken { 5 | #[prost(int32, tag = "1")] 6 | pub uid: i32, 7 | #[prost(string, tag = "2")] 8 | pub jwt_string: ::prost::alloc::string::String, 9 | } 10 | -------------------------------------------------------------------------------- /service-v3/src/service/gen/typing.rs: -------------------------------------------------------------------------------- 1 | /// 校区定义 2 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] 3 | #[repr(i32)] 4 | pub enum Campus { 5 | /// 徐汇校区 6 | Xuhui = 0, 7 | /// 奉贤校区 8 | Fengxian = 1, 9 | } 10 | impl Campus { 11 | /// String value of the enum field names used in the ProtoBuf definition. 12 | /// 13 | /// The values are not transformed in any way and thus are considered stable 14 | /// (if the ProtoBuf definition does not change) and safe for programmatic use. 15 | pub fn as_str_name(&self) -> &'static str { 16 | match self { 17 | Campus::Xuhui => "Xuhui", 18 | Campus::Fengxian => "Fengxian", 19 | } 20 | } 21 | /// Creates an enum from field names used in the ProtoBuf definition. 22 | pub fn from_str_name(value: &str) -> ::core::option::Option { 23 | match value { 24 | "Xuhui" => Some(Self::Xuhui), 25 | "Fengxian" => Some(Self::Fengxian), 26 | _ => None, 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /service-v3/src/service/gen/user.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | /// 小风筝用户信息 20 | #[allow(clippy::derive_partial_eq_without_eq)] 21 | #[derive(Clone, PartialEq, ::prost::Message)] 22 | pub struct User { 23 | /// uid 24 | #[prost(int32, tag = "1")] 25 | pub uid: i32, 26 | /// 账号,为学生学号,或教师工号。4、9或10位字母或数字 27 | /// 部分用户可能使用 authserver 的别名功能 28 | #[prost(string, tag = "2")] 29 | pub account: ::prost::alloc::string::String, 30 | /// 账号创建时间 31 | #[prost(message, optional, tag = "3")] 32 | pub create_time: ::core::option::Option<::prost_types::Timestamp>, 33 | } 34 | /// OA 登录凭据 35 | #[allow(clippy::derive_partial_eq_without_eq)] 36 | #[derive(Clone, PartialEq, ::prost::Message)] 37 | pub struct OaCredential { 38 | /// 账号,详见 User.account 描述 39 | #[prost(string, tag = "1")] 40 | pub account: ::prost::alloc::string::String, 41 | /// OA 密码 42 | #[prost(string, tag = "2")] 43 | pub password: ::prost::alloc::string::String, 44 | } 45 | /// 登录过程, client -> kite-server 流数据 46 | #[allow(clippy::derive_partial_eq_without_eq)] 47 | #[derive(Clone, PartialEq, ::prost::Message)] 48 | pub struct ClientStream { 49 | #[prost(oneof = "client_stream::Payload", tags = "1, 2")] 50 | pub payload: ::core::option::Option, 51 | } 52 | /// Nested message and enum types in `ClientStream`. 53 | pub mod client_stream { 54 | #[allow(clippy::derive_partial_eq_without_eq)] 55 | #[derive(Clone, PartialEq, ::prost::Oneof)] 56 | pub enum Payload { 57 | /// OA 凭据 58 | #[prost(message, tag = "1")] 59 | Credential(super::OaCredential), 60 | /// 来自 authserver 的 TLS 流数据,经由 client 转发到 kite-server 61 | #[prost(bytes, tag = "2")] 62 | TlsStream(::prost::alloc::vec::Vec), 63 | } 64 | } 65 | /// 登录过程, kite-server -> app 66 | #[allow(clippy::derive_partial_eq_without_eq)] 67 | #[derive(Clone, PartialEq, ::prost::Message)] 68 | pub struct ServerStream { 69 | #[prost(oneof = "server_stream::Payload", tags = "1, 2, 3")] 70 | pub payload: ::core::option::Option, 71 | } 72 | /// Nested message and enum types in `ServerStream`. 73 | pub mod server_stream { 74 | #[allow(clippy::derive_partial_eq_without_eq)] 75 | #[derive(Clone, PartialEq, ::prost::Oneof)] 76 | pub enum Payload { 77 | /// 用户登录成功凭据 78 | #[prost(message, tag = "1")] 79 | User(super::User), 80 | /// 来自 kite-server 的数据,经由 client 发往 authserver 的流数据 81 | #[prost(bytes, tag = "2")] 82 | TlsStream(::prost::alloc::vec::Vec), 83 | /// 用户登录失败的错误提示 84 | #[prost(string, tag = "3")] 85 | Message(::prost::alloc::string::String), 86 | } 87 | } 88 | /// Generated server implementations. 89 | pub mod user_service_server { 90 | #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] 91 | 92 | use tonic::codegen::*; 93 | 94 | /// Generated trait containing gRPC methods that should be implemented for use with UserServiceServer. 95 | #[async_trait] 96 | pub trait UserService: Send + Sync + 'static { 97 | /// Server streaming response type for the Login method. 98 | type LoginStream: futures_core::Stream> + Send + 'static; 99 | /// 登录小风筝账户 100 | /// 101 | /// 受限于若干网络上的限制,需要使用用户侧手机作为 socks5 代理使用。该登录方案的原理是,建立一条 kite-server 和 102 | /// authserver.sit.edu.cn 之间的 TLS 连接,以确保通信不被用户(也可能是潜在的攻击者)监听和篡改。 103 | /// 该方案保证 server 可以可靠地验证用户提供的用户名和密码,同时避免了 IP 重试次数过多被防火墙封禁。 104 | async fn login( 105 | &self, 106 | request: tonic::Request>, 107 | ) -> Result, tonic::Status>; 108 | } 109 | #[derive(Debug)] 110 | pub struct UserServiceServer { 111 | inner: _Inner, 112 | accept_compression_encodings: EnabledCompressionEncodings, 113 | send_compression_encodings: EnabledCompressionEncodings, 114 | } 115 | struct _Inner(Arc); 116 | impl UserServiceServer { 117 | pub fn new(inner: T) -> Self { 118 | Self::from_arc(Arc::new(inner)) 119 | } 120 | pub fn from_arc(inner: Arc) -> Self { 121 | let inner = _Inner(inner); 122 | Self { 123 | inner, 124 | accept_compression_encodings: Default::default(), 125 | send_compression_encodings: Default::default(), 126 | } 127 | } 128 | pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService 129 | where 130 | F: tonic::service::Interceptor, 131 | { 132 | InterceptedService::new(Self::new(inner), interceptor) 133 | } 134 | /// Enable decompressing requests with the given encoding. 135 | #[must_use] 136 | pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 137 | self.accept_compression_encodings.enable(encoding); 138 | self 139 | } 140 | /// Compress responses with the given encoding, if the client supports it. 141 | #[must_use] 142 | pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 143 | self.send_compression_encodings.enable(encoding); 144 | self 145 | } 146 | } 147 | impl tonic::codegen::Service> for UserServiceServer 148 | where 149 | T: UserService, 150 | B: Body + Send + 'static, 151 | B::Error: Into + Send + 'static, 152 | { 153 | type Response = http::Response; 154 | type Error = std::convert::Infallible; 155 | type Future = BoxFuture; 156 | fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { 157 | Poll::Ready(Ok(())) 158 | } 159 | fn call(&mut self, req: http::Request) -> Self::Future { 160 | let inner = self.inner.clone(); 161 | match req.uri().path() { 162 | "/user.UserService/Login" => { 163 | #[allow(non_camel_case_types)] 164 | struct LoginSvc(pub Arc); 165 | impl tonic::server::StreamingService for LoginSvc { 166 | type Response = super::ServerStream; 167 | type ResponseStream = T::LoginStream; 168 | type Future = BoxFuture, tonic::Status>; 169 | fn call( 170 | &mut self, 171 | request: tonic::Request>, 172 | ) -> Self::Future { 173 | let inner = self.0.clone(); 174 | let fut = async move { (*inner).login(request).await }; 175 | Box::pin(fut) 176 | } 177 | } 178 | let accept_compression_encodings = self.accept_compression_encodings; 179 | let send_compression_encodings = self.send_compression_encodings; 180 | let inner = self.inner.clone(); 181 | let fut = async move { 182 | let inner = inner.0; 183 | let method = LoginSvc(inner); 184 | let codec = tonic::codec::ProstCodec::default(); 185 | let mut grpc = tonic::server::Grpc::new(codec) 186 | .apply_compression_config(accept_compression_encodings, send_compression_encodings); 187 | let res = grpc.streaming(method, req).await; 188 | Ok(res) 189 | }; 190 | Box::pin(fut) 191 | } 192 | _ => Box::pin(async move { 193 | Ok(http::Response::builder() 194 | .status(200) 195 | .header("grpc-status", "12") 196 | .header("content-type", "application/grpc") 197 | .body(empty_body()) 198 | .unwrap()) 199 | }), 200 | } 201 | } 202 | } 203 | impl Clone for UserServiceServer { 204 | fn clone(&self) -> Self { 205 | let inner = self.inner.clone(); 206 | Self { 207 | inner, 208 | accept_compression_encodings: self.accept_compression_encodings, 209 | send_compression_encodings: self.send_compression_encodings, 210 | } 211 | } 212 | } 213 | impl Clone for _Inner { 214 | fn clone(&self) -> Self { 215 | Self(self.0.clone()) 216 | } 217 | } 218 | impl std::fmt::Debug for _Inner { 219 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 220 | write!(f, "{:?}", self.0) 221 | } 222 | } 223 | impl tonic::server::NamedService for UserServiceServer { 224 | const NAME: &'static str = "user.UserService"; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /service-v3/src/service/gen/yellow_page.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | /// 联系人信息 20 | #[allow(clippy::derive_partial_eq_without_eq)] 21 | #[derive(Clone, PartialEq, ::prost::Message)] 22 | pub struct Contact { 23 | /// 部门 24 | #[prost(string, optional, tag = "1")] 25 | pub department: ::core::option::Option<::prost::alloc::string::String>, 26 | /// 姓名 27 | #[prost(string, optional, tag = "2")] 28 | pub name: ::core::option::Option<::prost::alloc::string::String>, 29 | /// 电话号码 30 | #[prost(string, tag = "3")] 31 | pub phone: ::prost::alloc::string::String, 32 | /// 其他描述信息,可能是该部门位置 33 | #[prost(string, optional, tag = "4")] 34 | pub description: ::core::option::Option<::prost::alloc::string::String>, 35 | } 36 | #[allow(clippy::derive_partial_eq_without_eq)] 37 | #[derive(Clone, PartialEq, ::prost::Message)] 38 | pub struct YellowPageResponse { 39 | #[prost(message, repeated, tag = "1")] 40 | pub contacts: ::prost::alloc::vec::Vec, 41 | } 42 | /// Generated server implementations. 43 | pub mod yellow_page_service_server { 44 | #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] 45 | 46 | use tonic::codegen::*; 47 | 48 | /// Generated trait containing gRPC methods that should be implemented for use with YellowPageServiceServer. 49 | #[async_trait] 50 | pub trait YellowPageService: Send + Sync + 'static { 51 | /// 请求黄页联系人列表 52 | async fn get_yellow_page( 53 | &self, 54 | request: tonic::Request, 55 | ) -> Result, tonic::Status>; 56 | } 57 | #[derive(Debug)] 58 | pub struct YellowPageServiceServer { 59 | inner: _Inner, 60 | accept_compression_encodings: EnabledCompressionEncodings, 61 | send_compression_encodings: EnabledCompressionEncodings, 62 | } 63 | struct _Inner(Arc); 64 | impl YellowPageServiceServer { 65 | pub fn new(inner: T) -> Self { 66 | Self::from_arc(Arc::new(inner)) 67 | } 68 | pub fn from_arc(inner: Arc) -> Self { 69 | let inner = _Inner(inner); 70 | Self { 71 | inner, 72 | accept_compression_encodings: Default::default(), 73 | send_compression_encodings: Default::default(), 74 | } 75 | } 76 | pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService 77 | where 78 | F: tonic::service::Interceptor, 79 | { 80 | InterceptedService::new(Self::new(inner), interceptor) 81 | } 82 | /// Enable decompressing requests with the given encoding. 83 | #[must_use] 84 | pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 85 | self.accept_compression_encodings.enable(encoding); 86 | self 87 | } 88 | /// Compress responses with the given encoding, if the client supports it. 89 | #[must_use] 90 | pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 91 | self.send_compression_encodings.enable(encoding); 92 | self 93 | } 94 | } 95 | impl tonic::codegen::Service> for YellowPageServiceServer 96 | where 97 | T: YellowPageService, 98 | B: Body + Send + 'static, 99 | B::Error: Into + Send + 'static, 100 | { 101 | type Response = http::Response; 102 | type Error = std::convert::Infallible; 103 | type Future = BoxFuture; 104 | fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { 105 | Poll::Ready(Ok(())) 106 | } 107 | fn call(&mut self, req: http::Request) -> Self::Future { 108 | let inner = self.inner.clone(); 109 | match req.uri().path() { 110 | "/yellow_page.YellowPageService/GetYellowPage" => { 111 | #[allow(non_camel_case_types)] 112 | struct GetYellowPageSvc(pub Arc); 113 | impl tonic::server::UnaryService for GetYellowPageSvc { 114 | type Response = super::YellowPageResponse; 115 | type Future = BoxFuture, tonic::Status>; 116 | fn call( 117 | &mut self, 118 | request: tonic::Request, 119 | ) -> Self::Future { 120 | let inner = self.0.clone(); 121 | let fut = async move { (*inner).get_yellow_page(request).await }; 122 | Box::pin(fut) 123 | } 124 | } 125 | let accept_compression_encodings = self.accept_compression_encodings; 126 | let send_compression_encodings = self.send_compression_encodings; 127 | let inner = self.inner.clone(); 128 | let fut = async move { 129 | let inner = inner.0; 130 | let method = GetYellowPageSvc(inner); 131 | let codec = tonic::codec::ProstCodec::default(); 132 | let mut grpc = tonic::server::Grpc::new(codec) 133 | .apply_compression_config(accept_compression_encodings, send_compression_encodings); 134 | let res = grpc.unary(method, req).await; 135 | Ok(res) 136 | }; 137 | Box::pin(fut) 138 | } 139 | _ => Box::pin(async move { 140 | Ok(http::Response::builder() 141 | .status(200) 142 | .header("grpc-status", "12") 143 | .header("content-type", "application/grpc") 144 | .body(empty_body()) 145 | .unwrap()) 146 | }), 147 | } 148 | } 149 | } 150 | impl Clone for YellowPageServiceServer { 151 | fn clone(&self) -> Self { 152 | let inner = self.inner.clone(); 153 | Self { 154 | inner, 155 | accept_compression_encodings: self.accept_compression_encodings, 156 | send_compression_encodings: self.send_compression_encodings, 157 | } 158 | } 159 | } 160 | 161 | impl Clone for _Inner { 162 | fn clone(&self) -> Self { 163 | Self(self.0.clone()) 164 | } 165 | } 166 | 167 | impl std::fmt::Debug for _Inner { 168 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 169 | write!(f, "{:?}", self.0) 170 | } 171 | } 172 | 173 | impl tonic::server::NamedService for YellowPageServiceServer { 174 | const NAME: &'static str = "yellow_page.YellowPageService"; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /service-v3/src/service/ping.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use tonic::{Request, Response, Status}; 20 | 21 | pub use crate::service::gen::ping as gen; 22 | 23 | #[tonic::async_trait] 24 | impl gen::ping_service_server::PingService for super::KiteGrpcServer { 25 | async fn ping(&self, request: Request) -> Result, Status> { 26 | let request = request.into_inner(); 27 | 28 | Ok(Response::new(gen::PongResponse { text: request.text })) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /service-v3/src/service/template.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | pub use convert::*; 20 | 21 | mod convert { 22 | use anyhow::Context; 23 | 24 | use crate::model::{PageSort, PageView, Uuid}; 25 | use crate::service::gen::template as gen; 26 | 27 | pub trait ToUuidMessage { 28 | fn uuid(self) -> gen::Uuid; 29 | } 30 | 31 | pub trait ToUuid { 32 | fn uuid(self) -> anyhow::Result; 33 | } 34 | 35 | pub trait ToPageView { 36 | fn page_option(self) -> PageView; 37 | } 38 | 39 | impl ToUuidMessage for Uuid { 40 | fn uuid(self) -> gen::Uuid { 41 | gen::Uuid { 42 | value: self.to_string(), 43 | } 44 | } 45 | } 46 | 47 | impl ToUuid for gen::Uuid { 48 | fn uuid(self) -> anyhow::Result { 49 | Uuid::parse_str(&self.value).with_context(|| format!("Failed to parse uuid: {}", self.value)) 50 | } 51 | } 52 | 53 | impl ToPageView for gen::PageOption { 54 | fn page_option(self) -> PageView { 55 | let sort = if self.sort.unwrap_or(0) == 0 { 56 | PageSort::Asc 57 | } else { 58 | PageSort::Desc 59 | }; 60 | PageView { 61 | size: self.size, 62 | index: self.index, 63 | sort, 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /service-v3/src/service/user.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * 上应小风筝 便利校园,一步到位 3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use std::pin::Pin; 20 | 21 | use anyhow::{anyhow, Result}; 22 | use sqlx::PgPool; 23 | use tokio::sync::mpsc; 24 | use tokio_stream::wrappers::ReceiverStream; 25 | use tokio_stream::StreamExt; 26 | use tonic::codegen::futures_core::Stream; 27 | use tonic::{Request, Response, Status, Streaming}; 28 | 29 | pub use stream::VirtualStream; 30 | 31 | use crate::authserver::{Credential, PortalConnector}; 32 | use crate::model::user; 33 | use crate::model::user::validate; 34 | pub use crate::service::gen::user as gen; 35 | use crate::service::gen::user::ClientStream; 36 | use crate::service::user::gen::User; 37 | 38 | type RpcClientPayload = gen::client_stream::Payload; 39 | type RpcServerPayload = gen::server_stream::Payload; 40 | type LoginResult = Result, Status>; 41 | type ResponseStream = Pin> + Send>>; 42 | 43 | mod stream { 44 | use std::future::Future; 45 | use std::pin::Pin; 46 | use std::task::{Context, Poll}; 47 | 48 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 49 | use tokio::sync::mpsc; 50 | 51 | use super::{RpcClientPayload, RpcServerPayload}; 52 | 53 | /// VirtualStream is a stream over in and out channels, which communicates with client by gRPC 54 | pub struct VirtualStream { 55 | rx_buffer: Vec, 56 | rx: mpsc::Receiver, 57 | tx: mpsc::Sender, 58 | } 59 | 60 | impl VirtualStream { 61 | pub fn new(rx: mpsc::Receiver, tx: mpsc::Sender) -> Self { 62 | Self { 63 | rx_buffer: Vec::with_capacity(1024), 64 | rx, 65 | tx, 66 | } 67 | } 68 | 69 | pub fn split(self) -> (mpsc::Receiver, mpsc::Sender) { 70 | (self.rx, self.tx) 71 | } 72 | } 73 | 74 | impl AsyncRead for VirtualStream { 75 | fn poll_read( 76 | mut self: Pin<&mut Self>, 77 | cx: &mut Context<'_>, 78 | buf: &mut ReadBuf<'_>, 79 | ) -> Poll> { 80 | // Copy bytes from this_frame to target. 81 | // If target doesn't fit, copy remaining bytes to rx_buffer and wait next polling. 82 | fn copy_buffer(rx_buffer: &mut Vec, this_frame: Vec, target: &mut ReadBuf<'_>) { 83 | let copy_size = std::cmp::min(this_frame.len(), target.remaining()); 84 | target.put_slice(&this_frame[..copy_size]); 85 | 86 | // If some bytes not copied yet, save them 87 | if copy_size < this_frame.len() { 88 | rx_buffer.extend_from_slice(&this_frame[copy_size..]); 89 | } 90 | } 91 | 92 | // If last polling doesn't finish, continue and return the remaining bytes. 93 | if !self.rx_buffer.is_empty() { 94 | let this_frame = self.rx_buffer.clone(); 95 | copy_buffer(&mut self.rx_buffer, this_frame, buf); 96 | return Poll::Ready(Ok(())); 97 | } 98 | // Try to poll from underlying channel :D 99 | match self.rx.poll_recv(cx) { 100 | Poll::Ready(payload) => { 101 | if let Some(RpcClientPayload::TlsStream(content)) = payload { 102 | if !content.is_empty() { 103 | copy_buffer(&mut self.rx_buffer, content, buf); 104 | } 105 | } 106 | Poll::Ready(Ok(())) 107 | } 108 | Poll::Pending => Poll::Pending, 109 | } 110 | } 111 | } 112 | 113 | impl AsyncWrite for VirtualStream { 114 | fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { 115 | let len = buf.len(); 116 | if len == 0 { 117 | return Poll::Ready(Ok(0)); 118 | } 119 | 120 | let payload = RpcServerPayload::TlsStream(buf.to_vec()); 121 | let fut = self.tx.send(payload); 122 | tokio::pin!(fut); 123 | 124 | use tokio::sync::mpsc::error::SendError; 125 | Future::poll(fut, cx) 126 | .map(|_state| Ok(len)) 127 | .map_err(|e: SendError| std::io::Error::new(std::io::ErrorKind::BrokenPipe, e)) 128 | } 129 | 130 | fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 131 | Poll::Ready(Ok(())) 132 | } 133 | 134 | fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 135 | Poll::Ready(Ok(())) 136 | } 137 | } 138 | } 139 | 140 | async fn stream_translation_task( 141 | db: PgPool, 142 | stream_in: Streaming, 143 | channel_out: mpsc::Sender>, 144 | ) { 145 | async fn stream_translation_task_inner( 146 | db: PgPool, 147 | stream_in: Streaming, 148 | channel_out: mpsc::Sender>, 149 | ) -> Result<()> { 150 | // Send message from here to login_task through this channel. 151 | let (tx_sender, tx_receiver) = mpsc::channel::(16); 152 | // Receive message here from login_task through this channel. 153 | let (rx_sender, mut rx_receiver) = mpsc::channel::(16); 154 | 155 | fn mapping_inbound_stream(element: Result) -> Result { 156 | match element { 157 | Ok(stream) => stream 158 | .payload 159 | .ok_or_else(|| anyhow!("Expect client payload from in_stream but received None.")), 160 | Err(status) => Err(status.into()), 161 | } 162 | } 163 | 164 | // Launch login_task, go!!! 165 | tokio::spawn(login_task(db, rx_sender, tx_receiver)); 166 | 167 | let mut in_stream = stream_in.map(mapping_inbound_stream); 168 | loop { 169 | tokio::select! { 170 | v = in_stream.next() => { 171 | let v: Result<_> = v.unwrap(); 172 | // maybe it's unnecessary to handle error returned by channel 173 | let _ = tx_sender.send(v?).await?; 174 | }, 175 | v = rx_receiver.recv() => { 176 | let payload_to_outer = gen::ServerStream {payload: v}; 177 | channel_out.send(Ok(payload_to_outer)).await?; 178 | }, 179 | } 180 | } 181 | } 182 | 183 | let result = stream_translation_task_inner(db, stream_in, channel_out).await; 184 | if let Err(e) = result { 185 | tracing::trace!("stream_translation_task exits with error {}, ", e); 186 | } 187 | } 188 | 189 | async fn login_task(db: PgPool, tx: mpsc::Sender, rx: mpsc::Receiver) { 190 | async fn login_task_inner( 191 | db: PgPool, 192 | tx: mpsc::Sender, 193 | mut rx: mpsc::Receiver, 194 | ) -> Result<()> { 195 | // Step 1: Get user credential from client 196 | let credential = if let Some(RpcClientPayload::Credential(oa)) = rx.recv().await { 197 | Credential::new(oa.account, oa.password) 198 | } else { 199 | return Err(anyhow!( 200 | "Unknown message received, RpcClientPayload::Credential expected." 201 | )); 202 | }; 203 | 204 | if !validate::check_username(&credential.account) { 205 | return Err(anyhow!("Only student and staff ID is supported.")); 206 | } 207 | 208 | // Step 2: Create virtual stream, merge tx & rx -> stream 209 | let stream = VirtualStream::new(rx, tx); 210 | let mut portal = PortalConnector::new().user(credential.clone()).bind(stream).await?; 211 | 212 | // Step 3: Do login 213 | portal.try_login().await?; 214 | 215 | // Step 4: Query database, (maybe register new account), get user profile. 216 | let user = if let Some(u) = user::query(&db, &credential.account).await? { 217 | u 218 | } else { 219 | let person_name = portal.get_person_name().await?; 220 | user::create(&db, &credential.account, &person_name).await? 221 | }; 222 | 223 | // Step 5: Recycle virtual stream 224 | let stream = portal.shutdown().await?; 225 | let (_rx, tx) = stream.split(); 226 | 227 | use crate::model::ToTimestamp; 228 | tx.send(RpcServerPayload::User(User { 229 | uid: user.uid, 230 | account: user.account, 231 | create_time: Some(ToTimestamp::datetime(user.create_time)), 232 | })) 233 | .await?; 234 | Ok(()) 235 | } 236 | 237 | if let Err(e) = login_task_inner(db, tx, rx).await { 238 | tracing::error!("Login task failed with error: {}", e); 239 | } 240 | } 241 | 242 | #[tonic::async_trait] 243 | impl gen::user_service_server::UserService for super::KiteGrpcServer { 244 | type LoginStream = ResponseStream; 245 | 246 | async fn login(&self, request: Request>) -> LoginResult { 247 | let in_stream = request.into_inner(); 248 | // Send message to remote through this channel, tx is used for stream_redirection_task, which 249 | // can transfer message to here, and then the ServerStream can arrived rx, and be redirected to 250 | // out_stream 251 | let (to_remote_tx, to_remote_rx) = mpsc::channel(16); 252 | let out_stream = ReceiverStream::new(to_remote_rx); 253 | 254 | tokio::spawn(stream_translation_task(self.db.clone(), in_stream, to_remote_tx)); 255 | // Function returns, but the stream continues... 256 | Ok(Response::new(Box::pin(out_stream) as Self::LoginStream)) 257 | } 258 | } 259 | --------------------------------------------------------------------------------