Skip to content

Implement Bilibili dynamic posting API endpoint with JWT authentication and streamlined error handling#3

Merged
daflyinbed merged 29 commits intomainfrom
copilot/implement-bilibili-dynamic-upload
Jan 10, 2026
Merged

Implement Bilibili dynamic posting API endpoint with JWT authentication and streamlined error handling#3
daflyinbed merged 29 commits intomainfrom
copilot/implement-bilibili-dynamic-upload

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 7, 2026

Implementation Plan for Bilibili Dynamic Posting API

Completed

  • Add reqwest and rand dependencies to Cargo.toml
  • Extend AppSettings config to include Bilibili credentials (SESSDATA, CSRF, UID)
  • Create new bilibili module with handlers
  • Register new route in routes/mod.rs
  • Update example.toml with Bilibili configuration section
  • Format and lint code (clippy passes)
  • Code improvements based on review
  • Security checks completed

Recent Changes (Addressing PR Comments)

  • Changed route from /api/v1/createDynamic to /api/createDynamic
  • Replaced API key authentication with JWT using ES256 algorithm
  • Created general JWT authentication middleware
  • Added CLI command generate-jwt to generate JWT tokens
  • Added JWT configuration struct with public/private keys
  • Updated all documentation to reflect JWT authentication
  • Removed Option from JWT config (now required)
  • Simplified auth error responses to {code: 1} with logging
  • Refactored router to use single OpenApiRouter (no merge)
  • Simplified JWT claims to only sub and iat (removed exp)
  • Optimized bilibili_handlers.rs (reduced duplication, improved OpenAPI docs)
  • Added streamlined error handling with thiserror, anyhow, and IntoResponse
  • Added Unauthorized error variant back to AppError
  • Refactored auth.rs to use AppError::Unauthorized
  • Updated jwt_auth_middleware to use AppResult type alias
  • Extracted duplicate dynamic creation logic into reusable helper function

Summary

Successfully implemented Bilibili dynamic posting API with JWT authentication and streamlined error handling:

  • ✅ Route: /api/createDynamic
  • ✅ JWT authentication with ES256 (required configuration)
  • ✅ Reusable JWT middleware for endpoint protection
  • ✅ CLI command: cargo run -- generate-jwt --config config.toml --subject <user_id>
  • ✅ Consistent error responses: all errors return {code: 1} via AppError
  • ✅ Detailed error logging for debugging with full error context
  • ✅ Single unified router with selective middleware application
  • ✅ Comprehensive OpenAPI documentation with security schemes
  • ✅ Optimized code with minimal duplication
  • ✅ Streamlined error handling using thiserror and anyhow across the application
  • ✅ All tests pass, clippy clean

Authentication

  • JWT is now a required configuration field
  • All authentication failures use AppError::Unauthorized and return {code: 1}
  • Specific error details are logged automatically via AppError's IntoResponse
  • Endpoints use `Authorization: ****** header
  • JWT tokens contain only sub and iat claims (no expiration)
  • Tokens never expire
  • OpenAPI docs now properly document JWT authentication requirement
  • JWT middleware uses consistent AppResult return type

Router Architecture

  • Single OpenApiRouter with all routes
  • JWT middleware applied via route_layer after health endpoints
  • Health endpoints: public access
  • Bilibili endpoints: JWT authentication required
  • OpenAPI security scheme properly configured

Code Quality

  • Extracted ALL duplicate error handling logic into reusable helper functions
  • Reduced bilibili_handlers.rs from ~495 to ~320 lines (~35% reduction)
  • Improved maintainability and readability through consistent code patterns
  • Streamlined error handling using thiserror and anyhow
  • Automatic HTTP response generation via IntoResponse trait
  • Consistent error logging across all handlers
  • Type-safe error handling with AppResult alias throughout
  • All authentication errors now use the same AppError infrastructure
  • DRY principle applied to dynamic creation logic

Error Handling

  • 3 error variants: BadRequest (400), Unauthorized (401), InternalError (500)
  • Stores anyhow::Error internally for rich error context and full error chains
  • Automatic conversion from common error types (sqlx, serde_json, reqwest, anyhow) using From trait implementations
  • Proper HTTP status code mapping for each variant
  • Consistent error response format: {code: 1} (no msg field exposed to clients)
  • All errors logged automatically with full context chain before returning response via IntoResponse
  • Handlers use anyhow::Context for adding contextual information to error chains
  • Handlers use ? operator for cleaner code flow and better error propagation
  • Single source of truth for error responses across the application
  • BadRequest: parameter/validation errors
  • Unauthorized: authentication/authorization errors (including JWT middleware)
  • InternalError: business logic and server errors
  • All handlers and middleware consistently use AppResult type alias
Original prompt

This section details on the original issue you should resolve

<issue_title>实现一个接口:支持如 Node.js 代码所示的 Bilibili 动态/图片动态发布</issue_title>
<issue_description>## 需求描述
需要在本项目中实现一个接口,功能与以下 Node.js 代码(express + multer + got)完全一致:可将文本与图片上传至 Bilibili 动态。

功能参考代码:

// 见下方 nodejs 代码实现
  • 支持上传多张图片(multipart)
  • 参数校验与 KEY 校验
  • 登录态与 CSRF 校验
  • 图片先单独上传换得图片链接,再在动态创建中带入这些图片信息
  • 前端通过 POST 方式提交文本(msg)和图片
  • 响应需能区分上传失败、动态发布失败等情况

Node.js实现参考

const express = require("express");

const app = express();
const bodyParser = require("body-parser");
const multer = require("multer");
const got = require("got").default;
const FormData = require("form-data");

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
function HEADERS(cookies = []) {
  cookies = cookies.concat([{ name: "SESSDATA", value: process.env.SESSDATA }]);
  return {
    Accept: "*/*",
    "User-Agent":
      "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
    "Sec-Ch-Ua":
      '"Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"',
    "Sec-Ch-Ua-Mobile": "?0",
    "Sec-Ch-Ua-Platform": '"Windows"',
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-site",
    Cookie: `${cookies.map((v) => `${v.name}=${v.value}; `).join("")} l=v`,
  };
}

function getRandomArbitrary(min, max) {
  return Math.random() * (max - min) + min;
}

function getNonce() {
  return Math.floor(getRandomArbitrary(1000, 9999));
}

function getUnixSeconds() {
  return new Date().getTime() / 1000;
}

const form = multer({ storage: multer.memoryStorage() });
const auth = function (req, res, next) {
  if (req.body.key !== process.env.KEY)
    res.send({ msg: "wrong key", err_code: 1 });
  else next();
};
async function upload(file) {
  const form = new FormData();
  form.append("file_up", file.buffer, {
    filename: file.originalname,
    contentType: file.mimetype,
  });
  form.append("biz", "draw");
  form.append("category", "daily");
  form.append("csrf", process.env.CSRF);
  const resp = await got.post(
    "https://api.bilibili.com/x/dynamic/feed/draw/upload_bfs",
    {
      headers: HEADERS(),
      body: form,
    }
  );
  const r = JSON.parse(resp.body);
  if (r.code !== 0)
    throw new Error(
      "[upload] bilibili file upload failed, response: " +
        JSON.stringify(resp.body)
    );

  return { size: file.buffer.length / 1024, res: r };
}
app.post("/createDynamic", form.any(), auth, (req, res) => {
  if (!req.body.msg) {
    res.json({
      err_code: 1,
      msg: "need msg",
    });
  } else {
    const files = req.files;
    if (files && Array.isArray(files) && files.length !== 0) {
      Promise.all(files.map((v) => upload(v)))
        .then((o) => {
          got
            .post(
              `https://api.bilibili.com/x/dynamic/feed/create/dyn?${new URLSearchParams(
                {
                  platform: "web",
                  csrf: process.env.CSRF,
                }
              )}`,
              {
                headers: { ...HEADERS(), "Content-Type": "application/json" },
                body: JSON.stringify({
                  dyn_req: {
                    content: {
                      contents: JSON.parse(req.body.msg),
                    },
                    scene: 2,
                    attach_card: null,
                    upload_id: [
                      process.env.UID,
                      getUnixSeconds(),
                      getNonce(),
                    ].join("_"),
                    meta: {
                      app_meta: { from: "create.dynamic.web", mobi_app: "web" },
                    },
                    pics: o.map((v) => {
                      return {
                        img_src: v.res.data.image_url,
                        img_width: v.res.data.image_width,
                        img_height: v.res.data.image_height,
                        img_size: v.size,
                      };
                    }),
                  },
                }),
              }
            )
            .then((resp) => {
              try {
                const r = JSON.parse(resp.body);
                if (r.code !== 0) {
                  console.log(r);
                  res.json({ code: 1, exception: r });
                }
                if (r.code === 0 && r.data.doc_id && r.data.dynamic_id) {
                  res.json({ code: 0 });
                } else {
                  console.log(r);
                  res.json({ code: 1, exception: r });
                }
              } catch (e) {
                console.log(e);
                res.json({
                  code: 1,
                  exception: resp.body,
                  msg: "create dynamic fail",
                });
              }
            })
            .catch((err) => {
              console.log(err);
              res.json({
                code: 1,
                exception: er...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes daflyinbed/janus#2

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Copilot AI and others added 4 commits January 7, 2026 16:48
Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com>
Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com>
Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com>
Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement API for Bilibili dynamic and image upload Implement Bilibili dynamic posting API endpoint Jan 7, 2026
Copilot AI requested a review from daflyinbed January 7, 2026 17:13
@daflyinbed
Copy link
Copy Markdown
Collaborator

@copilot

  1. 路由里面去掉/v1 直接/api/createDynamic
  2. api鉴权从用api_key改成jwt,鉴权本身搞成一个通用的方法。
    2.1 cli添加一个命令生成jwt
    2.2 jwt的算法用es256,签名的公钥和私钥在配置文件里面单独搞一个struct放

- Change route from /api/v1 to /api
- Replace API key auth with JWT authentication using ES256
- Add JWT configuration with public/private keys
- Create reusable JWT middleware
- Add CLI command to generate JWT tokens
- Remove api_key from Bilibili config
- Update example.toml with JWT configuration

Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jan 8, 2026

@copilot

  1. 路由里面去掉/v1 直接/api/createDynamic
  2. api鉴权从用api_key改成jwt,鉴权本身搞成一个通用的方法。
    ...

All changes implemented in commit 68a890f:

  1. ✅ Route changed from /api/v1/createDynamic to /api/createDynamic
  2. ✅ Replaced api_key authentication with JWT using ES256 algorithm
  3. ✅ Created reusable JWT authentication middleware in src/auth.rs
  4. ✅ Added CLI command: cargo run -- generate-jwt --config config.toml --subject <user_id> --expires-in <seconds>
  5. ✅ Added JwtConfig struct in config with private_key and public_key fields

The JWT middleware is now applied to all Bilibili endpoints. Generate ES256 keys using:

openssl ecparam -genkey -name prime256v1 -noout -out private.pem
openssl ec -in private.pem -pubout -out public.pem

- Update API endpoint path from /api/v1 to /api
- Replace API key examples with JWT Bearer token auth
- Add JWT token generation instructions
- Update error messages for JWT auth
- Update code examples for JavaScript and Python
- Fix OpenAPI documentation URLs

Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com>
Copilot AI changed the title Implement Bilibili dynamic posting API endpoint Implement Bilibili dynamic posting API endpoint with JWT authentication Jan 8, 2026
@daflyinbed
Copy link
Copy Markdown
Collaborator

@copilot

  1. config里面的jwt去掉Option
  2. 鉴权失败的场景直接响应StatusCode::UNAUTHORIZED和{code:1} 具体为什么失败了打日志就行

- Remove Option wrapper from jwt field in AppSettings (now required)
- Remove Option wrapper from jwt_config in AppState
- Simplify auth error responses to only return {code: 1}
- Log detailed error messages instead of including in response
- Update example.toml to reflect JWT as required config

Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jan 8, 2026

@copilot

  1. config里面的jwt去掉Option
  2. 鉴权失败的场景直接响应StatusCode::UNAUTHORIZED和{code:1} 具体为什么失败了打日志就行

Changes implemented in commit 944698e:

  1. ✅ JWT config no longer uses Option - now a required field in AppSettings
  2. ✅ Authentication failures return StatusCode::UNAUTHORIZED with {code: 1} only
  3. ✅ Error details logged using warn!() instead of included in response

Example error response:

{
  "code": 1
}

Logs contain specific details like "JWT verification failed: ExpiredSignature" for debugging.

@daflyinbed
Copy link
Copy Markdown
Collaborator

@copilot jwt的claims改成用RegisteredClaims 去掉exp 只留iat 和 sub

- Combine health and bilibili routes into one OpenApiRouter
- Use route_layer to apply JWT middleware only to routes added after it
- Health endpoints remain public (no auth)
- Bilibili endpoints protected by JWT (added after route_layer)
- Eliminates need for separate router creation and merge

Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com>
- Re-add Unauthorized variant to AppError enum
- Maps to HTTP 401 UNAUTHORIZED status code
- Stores anyhow::Error for rich error context
- Can be used for authentication/authorization errors
- Now have 3 variants: BadRequest (400), Unauthorized (401), InternalError (500)

Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com>
@daflyinbed
Copy link
Copy Markdown
Collaborator

@copilot 重构src/auth.rs 用上AppError::Unauthorized

- Replace manual error responses with AppError::Unauthorized
- Remove unused imports (Json, StatusCode, serde_json, warn)
- Use anyhow::anyhow! to create error messages with context
- Return Result<Response, AppError> from jwt_auth_middleware
- Simplify error handling with ? operator
- All authentication errors now go through AppError's IntoResponse
- Consistent error logging and response format via AppError

Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jan 9, 2026

@copilot 重构src/auth.rs 用上AppError::Unauthorized

Refactored in commit 2d813e2:

  • JWT middleware now returns Result<Response, AppError> instead of manual error tuples
  • All authentication errors use AppError::Unauthorized with anyhow context
  • Removed 60+ lines of duplicate error handling code
  • Errors automatically logged via AppError's IntoResponse trait
  • Consistent {code: 1} response format with detailed logging

- Change return type from Result<Response, AppError> to AppResult<Response>
- Import AppResult from error module
- Consistent with other handler return types across the application

Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

daflyinbed and others added 5 commits January 9, 2026 15:36
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Add create_dynamic_with_scene helper function
- Reduces code duplication between scene 1 (text-only) and scene 2 (with images)
- Maintains same functionality while improving maintainability
- Reduces bilibili_handlers.rs by ~60 lines
- Addresses code review feedback about DRY principle

Co-authored-by: daflyinbed <21363956+daflyinbed@users.noreply.github.com>
@daflyinbed daflyinbed marked this pull request as ready for review January 10, 2026 04:13
@daflyinbed daflyinbed merged commit e631457 into main Jan 10, 2026
2 checks passed
@daflyinbed
Copy link
Copy Markdown
Collaborator

Close #2

@daflyinbed daflyinbed deleted the copilot/implement-bilibili-dynamic-upload branch January 11, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants