Skip to content

Add HTML imgbed upload support#162

Open
clown145 wants to merge 4 commits intoSXP-Simon:mainfrom
clown145:feat/html-imgbed-upload
Open

Add HTML imgbed upload support#162
clown145 wants to merge 4 commits intoSXP-Simon:mainfrom
clown145:feat/html-imgbed-upload

Conversation

@clown145
Copy link
Copy Markdown
Contributor

@clown145 clown145 commented Apr 8, 2026

User description

添加把HTML上传到图床的支持

Summary by Sourcery

Add an HTML report publisher that can optionally upload generated HTML reports to an external imgbed and fall back to sending local files when needed.

New Features:

  • Introduce HtmlReportPublisher to handle publishing HTML reports via direct file send or external imgbed upload with URL-based captions.
  • Add configuration options to enable HTML upload, provide an upload token, and select an upload channel for HTML reports.

Enhancements:

  • Refactor HTML report dispatching in group analysis and reporting to use the centralized HtmlReportPublisher instead of inline send logic.
  • Update README documentation to describe HTML imgbed upload configuration, behavior, and compatibility notes with CloudFlare-ImgBed.

Documentation:

  • Document HTML report imgbed upload workflow, configuration flags, and self-hosted vs imgbed behavior in the README.

PR Type

Enhancement, Documentation


Description

  • Introduce HtmlReportPublisher to upload HTML reports to imgbed

  • Add configuration options for imgbed upload (token, channel, enable)

  • Update reporting dispatch to use the new publisher with fallback

  • Document the imgbed upload workflow in README


Diagram Walkthrough

flowchart LR
  A["Generate HTML report"] --> B{"html_upload_enabled?"}
  B -- "Yes" --> C["Upload to imgbed"]
  C --> D{"Upload success?"}
  D -- "Yes" --> E["Send imgbed URL"]
  D -- "No" --> F["Send local file"]
  B -- "No" --> F
Loading

File Walkthrough

Relevant files
Enhancement
4 files
main.py
Integrate HtmlReportPublisher for HTML dispatch                   
+11/-15 
config_manager.py
Add HTML upload configuration methods                                       
+12/-0   
dispatcher.py
Use HtmlReportPublisher for HTML reports                                 
+3/-4     
html_publisher.py
Implement HtmlReportPublisher class                                           
+221/-0 
Miscellaneous
1 files
__init__.py
Export HtmlReportPublisher                                                             
+2/-1     
Documentation
1 files
README.md
Document HTML imgbed upload workflow                                         
+14/-5   
Configuration changes
1 files
_conf_schema.json
Add html_upload_enabled and related config                             
+27/-1   

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 8, 2026

Reviewer's Guide

Adds an HtmlReportPublisher to centralize HTML report delivery and introduce optional upload to an HTML imgbed (CloudFlare-ImgBed style), with corresponding config options and README documentation updates.

Sequence diagram for HTML report publishing with optional imgbed upload

sequenceDiagram
    participant G as GroupDailyAnalysis
    participant RD as ReportDispatcher
    participant HRP as HtmlReportPublisher
    participant CM as ConfigManager
    participant MS as MessageSender
    participant IB as ImgBedServer

    G->>HRP: publish(group_id, html_path, platform_id)
    activate HRP
    HRP->>CM: get_html_upload_enabled()
    CM-->>HRP: enabled_flag
    alt upload_enabled
        HRP->>HRP: upload(html_path)
        activate HRP
        HRP->>CM: get_html_base_url(), get_html_upload_token(), get_html_upload_channel()
        CM-->>HRP: base_url, token, channel
        HRP->>IB: POST /upload (file, token, channel)
        IB-->>HRP: JSON payload (src or error)
        HRP->>HRP: _extract_src(payload)
        HRP-->>HRP: public_url or None
        deactivate HRP
        alt upload_success
            HRP->>HRP: build_caption(public_url=public_url)
            HRP->>MS: send_text(group_id, caption, platform_id)
            MS-->>HRP: sent_true_or_false
            alt text_sent_ok
                HRP-->>G: True
            else text_send_failed
                HRP->>MS: send_file(group_id, html_path, caption, platform_id)
                MS-->>HRP: sent_result
                HRP-->>G: sent_result
            end
        else upload_failed
            HRP->>MS: send_file(group_id, html_path, caption, platform_id)
            MS-->>HRP: sent_result
            HRP-->>G: sent_result
        end
    else upload_disabled
        HRP->>HRP: build_caption(html_path=html_path)
        HRP->>MS: send_file(group_id, html_path, caption, platform_id)
        MS-->>HRP: sent_result
        HRP-->>G: sent_result
    end
Loading

Class diagram for HTML report publishing with HtmlReportPublisher

classDiagram
    class HtmlReportPublisher {
        - config_manager
        - message_sender
        + HtmlReportPublisher(config_manager, message_sender)
        + build_caption(html_path, public_url) str
        + publish(group_id, html_path, platform_id) bool
        + upload(html_path) str
        - _normalized_base_url() str
        - _build_self_hosted_url(html_path) str
        - _build_public_url(src) str
        - _extract_src(payload) str
    }

    class ConfigManager {
        + get_html_base_url() str
        + get_html_upload_enabled() bool
        + get_html_upload_token() str
        + get_html_upload_channel() str
        + get_html_output_dir() str
    }

    class MessageSender {
        + send_text(group_id, text, platform_id) bool
        + send_file(group_id, file_path, caption, platform_id) bool
    }

    class ReportDispatcher {
        - config_manager
        - report_generator
        - message_sender
        - html_report_publisher
        + ReportDispatcher(config_manager, report_generator, message_sender)
        + set_html_render(render_func)
        + dispatch(...)
        - _dispatch_html(group_id, platform_id, trace_context)
    }

    class GroupDailyAnalysis {
        - config_manager
        - bot_manager
        - analysis_service
        - report_generator
        - message_sender
        - html_report_publisher
        + GroupDailyAnalysis(context, config)
        + some_handler_methods()
    }

    HtmlReportPublisher --> ConfigManager : uses
    HtmlReportPublisher --> MessageSender : uses

    ReportDispatcher --> HtmlReportPublisher : composes
    GroupDailyAnalysis --> HtmlReportPublisher : composes

    ReportDispatcher --> MessageSender
    GroupDailyAnalysis --> MessageSender
    ConfigManager <.. ReportDispatcher
    ConfigManager <.. GroupDailyAnalysis
Loading

File-Level Changes

Change Details Files
Introduce HtmlReportPublisher to encapsulate HTML report caption building, optional imgbed upload, and fallback local file sending.
  • Create HtmlReportPublisher class that can build captions including public URLs or self-hosted links.
  • Implement upload logic using aiohttp to POST HTML files to an imgbed /upload endpoint with token and optional channel, parsing various JSON response shapes to extract src.
  • Add helpers to normalize base URL, construct self-hosted URLs from html_output_dir, and build final public URLs from imgbed src values.
src/infrastructure/reporting/html_publisher.py
Wire HtmlReportPublisher into existing report flows for scheduled and on-demand HTML reports.
  • Add html_report_publisher dependency to GroupDailyAnalysis, constructing it with the existing ConfigManager and MessageSender.
  • Update HTML report handling in GroupDailyAnalysis to call html_report_publisher.publish and, on failure, send the raw HTML file with a caption from HtmlReportPublisher.
  • Update ReportDispatcher to use HtmlReportPublisher.publish instead of directly sending the HTML file with ReportGenerator’s caption logic.
  • Export HtmlReportPublisher from the reporting package for wider reuse.
main.py
src/infrastructure/reporting/dispatcher.py
src/infrastructure/reporting/__init__.py
Extend configuration to support toggling HTML upload and providing imgbed credentials/channel.
  • Add getters for html_upload_enabled, html_upload_token, and html_upload_channel on ConfigManager, with sensible defaults.
  • Use these new getters inside HtmlReportPublisher to control upload behavior and request parameters.
src/infrastructure/config/config_manager.py
_conf_schema.json
Document HTML imgbed upload behavior and configuration in README, including self-hosted vs imgbed modes and CloudFlare-ImgBed compatibility notes.
  • Rename the HTML report section to include self-hosted and imgbed upload, describing the new config flags and behavior.
  • Clarify how html_base_url is interpreted in self-hosted vs imgbed modes and describe fallback behavior when upload fails.
  • Add a collapsible section explaining compatibility with CloudFlare-ImgBed and the expectation of an /upload endpoint at html_base_url.
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • When html_upload_enabled is true and the upload fails, build_caption will never include a self-hosted html_base_url link because it explicitly skips _build_self_hosted_url in that mode; consider basing the decision on whether public_url is present instead of the html_upload_enabled flag so the caption still contains a usable link on fallback.
  • In _build_public_url, absolute src values (starting with http:// or https://) are forcibly remapped under html_base_url, which can break correct external URLs returned by some providers; it may be safer to return absolute URLs as-is and only join html_base_url for relative paths.
  • In upload, the broad except Exception as e only logs the message string; switching to logger.exception (or including exc_info=True) would make diagnosing upload issues easier by recording stack traces.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- When `html_upload_enabled` is true and the upload fails, `build_caption` will never include a self-hosted `html_base_url` link because it explicitly skips `_build_self_hosted_url` in that mode; consider basing the decision on whether `public_url` is present instead of the `html_upload_enabled` flag so the caption still contains a usable link on fallback.
- In `_build_public_url`, absolute `src` values (starting with `http://` or `https://`) are forcibly remapped under `html_base_url`, which can break correct external URLs returned by some providers; it may be safer to return absolute URLs as-is and only join `html_base_url` for relative paths.
- In `upload`, the broad `except Exception as e` only logs the message string; switching to `logger.exception` (or including `exc_info=True`) would make diagnosing upload issues easier by recording stack traces.

## Individual Comments

### Comment 1
<location path="src/infrastructure/reporting/html_publisher.py" line_range="182-189" />
<code_context>
+            return None
+
+        src = str(src).strip()
+        if src.startswith(("http://", "https://")):
+            parsed = urlsplit(src)
+            relative = parsed.path or "/"
+            if parsed.query:
+                relative = f"{relative}?{parsed.query}"
+            if parsed.fragment:
+                relative = f"{relative}#{parsed.fragment}"
+            return (
+                f"{base_url}{relative if relative.startswith('/') else '/' + relative}"
+            )
</code_context>
<issue_to_address>
**question (bug_risk):** Rewriting absolute `src` URLs with `html_base_url` may be surprising if the uploader returns fully qualified public URLs.

In `_build_public_url`, absolute `src` values are re-mapped onto `html_base_url`, dropping their original scheme/host and keeping only path/query/fragment. This works if the uploader returns internal URLs that must be exposed via a public gateway, but will break setups where the uploader already returns public URLs. Consider preserving absolute URLs as-is and only applying `html_base_url` to relative paths.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +182 to +189
if src.startswith(("http://", "https://")):
parsed = urlsplit(src)
relative = parsed.path or "/"
if parsed.query:
relative = f"{relative}?{parsed.query}"
if parsed.fragment:
relative = f"{relative}#{parsed.fragment}"
return (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question (bug_risk): Rewriting absolute src URLs with html_base_url may be surprising if the uploader returns fully qualified public URLs.

In _build_public_url, absolute src values are re-mapped onto html_base_url, dropping their original scheme/host and keeping only path/query/fragment. This works if the uploader returns internal URLs that must be exposed via a public gateway, but will break setups where the uploader already returns public URLs. Consider preserving absolute URLs as-is and only applying html_base_url to relative paths.

@clown145
Copy link
Copy Markdown
Contributor Author

clown145 commented Apr 8, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd86f47554

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +88 to +89
if channel and channel != "default":
params["uploadChannel"] = channel
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve default to a concrete upload channel

When html_upload_channel is left as default, this code intentionally omits uploadChannel from the /upload request. In CloudFlare-ImgBed's API, the documented default for uploadChannel is telegram, so deployments that only configure cfr2/s3/discord/etc. can fail uploads and always fall back to local-file sending despite upload mode being enabled. This should map default to an actual available channel (or validate against /api/channels) instead of silently dropping the parameter.

Useful? React with 👍 / 👎.

@SXP-Simon
Copy link
Copy Markdown
Owner

Preparing review...

1 similar comment
@SXP-Simon
Copy link
Copy Markdown
Owner

Preparing review...

@SXP-Simon
Copy link
Copy Markdown
Owner

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ No major issues detected

@SXP-Simon
Copy link
Copy Markdown
Owner

PR Description updated to latest commit (ac7a0e4)

@SXP-Simon
Copy link
Copy Markdown
Owner

SXP-Simon commented Apr 27, 2026

PR Code Suggestions ✨

CategorySuggestion                                                                                                                                    Impact
Possible issue
Return absolute URLs unchanged

When an image bed returns an absolute URL (starting with http:// or https://), the
current implementation reconstructs it using base_url, effectively replacing the
original domain. This likely produces a broken link if the image bed uses a CDN or a
different host for serving files. The improved code returns the original absolute
src unchanged, which is more reliable and aligns with typical image bed APIs.

src/infrastructure/reporting/html_publisher.py [176-193]

 def _build_public_url(self, src: str) -> str | None:
     base_url = self._normalized_base_url()
     if not base_url or not src:
         return None
 
     src = str(src).strip()
+    # If the image bed already returns an absolute URL, trust it as-is.
     if src.startswith(("http://", "https://")):
-        parsed = urlsplit(src)
-        relative = parsed.path or "/"
-        if parsed.query:
-            relative = f"{relative}?{parsed.query}"
-        if parsed.fragment:
-            relative = f"{relative}#{parsed.fragment}"
-        return (
-            f"{base_url}{relative if relative.startswith('/') else '/' + relative}"
-        )
+        return src
 
     return f"{base_url}/{src.lstrip('/')}"
Suggestion importance[1-10]: 10

__

Why: The original _build_public_url method incorrectly reconstructs absolute URLs returned by the image bed, potentially breaking links. Returning them as-is fixes a critical bug in HTML report publishing, ensuring correct public URLs are sent.

High

@SXP-Simon
Copy link
Copy Markdown
Owner

PR Description updated to latest commit (ac7a0e4)

Comment on lines +176 to +193
def _build_public_url(self, src: str) -> str | None:
base_url = self._normalized_base_url()
if not base_url or not src:
return None

src = str(src).strip()
if src.startswith(("http://", "https://")):
parsed = urlsplit(src)
relative = parsed.path or "/"
if parsed.query:
relative = f"{relative}?{parsed.query}"
if parsed.fragment:
relative = f"{relative}#{parsed.fragment}"
return (
f"{base_url}{relative if relative.startswith('/') else '/' + relative}"
)

return f"{base_url}/{src.lstrip('/')}"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggestion: When the “src” returned by the image bed is an absolute URL (e.g., https://imgbed.example.com/files/abc.html), the current logic strips the host and prepends base_url again, which can produce a broken link. Instead, return the absolute src unchanged, since the base URL is already configured as the image bed’s root. [possible issue, importance: 8]

Suggested change
def _build_public_url(self, src: str) -> str | None:
base_url = self._normalized_base_url()
if not base_url or not src:
return None
src = str(src).strip()
if src.startswith(("http://", "https://")):
parsed = urlsplit(src)
relative = parsed.path or "/"
if parsed.query:
relative = f"{relative}?{parsed.query}"
if parsed.fragment:
relative = f"{relative}#{parsed.fragment}"
return (
f"{base_url}{relative if relative.startswith('/') else '/' + relative}"
)
return f"{base_url}/{src.lstrip('/')}"
def _build_public_url(self, src: str) -> str | None:
base_url = self._normalized_base_url()
if not base_url or not src:
return None
src = str(src).strip()
if src.startswith(("http://", "https://")):
return src # already absolute – use as-is
return f"{base_url}/{src.lstrip('/')}"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants