forgejo/modules
Mathieu Fenniak 77fafbe578 Add a "summary card" to issues & PRs for consumption by OpenGraph clients (#6053)
## Overview

Hi all, I'm a first-time contributor to Forgejo.  I was looking for something interesting to contribute and the first thing that caught my attention was https://codeberg.org/forgejo/forgejo/issues/6043, a request for an enhancement to include "issue previews" when publishing links to social media platforms.  As a bit of background, the way these platforms work is that they search for meta tags in the posted link's content, and if they find a meta `og:image` (along with other meta tags) they'll pull the image to include in the social media post.  Forgejo currently provides an `og:image` tag but it just renders the repository or repository-owner's avatar.

This PR will render `og:image` for an issue or PR into a link to `{...}/summary-card`, which is a dynamically generated image that contains a summary of the issue.

## Design Notes

### Rendering / Rasterization

The tricky part of solving this problem is rendering an image that combines some text, some images, and some layout elements.  To address this, I've created a `card` module which allows for a handful of operations:
- Create a new rendered image (a "Card")
- Add a margin to a card
- Split the card, horizontally or vertically, into two pieces with a proportional layout (eg. 70%/30%, as desired), each of which are "Cards" that render into the same root image
- Render text into a card, with line-wrapping and text-alignment capabilities
- Render an image onto a card
- Fetches an external image as safely as possible (for server-side fetch of Gravatar, etc.)

The card module can be reused to create `og:image` summary cards for any object in the future, although obviously it's capabilities are limited.  The current implementation is on issues/PRs.

I considered a few alternative approaches before taking this approach, and here's why I rejected those options:
- Provide the summary card as an SVG object which could be rendered much more easily with a template file -- however, support for SVG isn't defined as positive for OpenGraph, and a quick look through some existing implementations suggest that it is not widely supported, if at all
- Rendering as HTML/CSS, or SVG, and then using an external tool to convert into a PNG (or other static) image -- this would be much nicer and easier to implement, but would require tying in some very heavy-weight dependencies
- Rendering using a more sophisticated graphics library, eg. cairo -- also would be nicer and easier to implement, but again a heavy dependency for a small functionality

As a result of the limited capabilities of the new card module, summary cards don't have icons on them (which would require SVG rasterization) or pretty status badges with colors and rounded rects.  In the future if better drawing capabilities were added, the graphics could be improved, but it doesn't seem too important.

### External Avatars

In order to rasterize a user's avatar onto the summary card, it might have to be retrieved by the server from the external source (eg. Gravatar).  A `fetchExternalImage` routine attempts to do this in the safest way possible to protect the server from any possible security exposure from this; (a) verifying that the content-types are acceptable, (b) ensuring that the file-size and image-size are within the safe bounds that are used for custom avatars, (c) using a very-short timeout to avoid stalling the server if an external dependency is offline.

### Caching

Summary cards are cached after rendered.  This has the downside of causing updates to statuses, avatars, titles, etc. being stale on the summary card for the cache TTL.  However, during testing I found that some social media engines like Mastodon will cause the summary card to be accessed a significant number of times after being referenced by a post, causing a mini-tornado of requests.  The cache compensates for this to avoid server load in this situation.

### Scope

I'm considering out-of-scope:
- Summary cards on other objects (eg. repos, users) can be left for future implementation

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- ~~I added test coverage for JavaScript changes...~~ n/a, no JS changes
  - [x] ~~in `web_src/js/*.test.js` if it can be unit tested.~~
  - [x] ~~in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).~~
- Manual testing
  - [x] Access & attach screenshots of both an issue and a pull-request's summary card; see below
  - [x] Ensure reasonable (non-crash) behavior of rendering text with glyphs outside the font -- correctly rendered as replacement unicode chars
  - [x] Using a public test instance, verify that og:image behavior looks good on platforms like Mastodon and BlueSky
    - [x] Bluesky: 
    - [x] Mastodon:    (Note that the summary card will be requested many times as the post is federated; either each server, or each client, will fetch it itself)
    - [x] OpenGraph test site (https://www.opengraph.xyz/): 
    - [x] Discord: Looks OK ; needs "twitter:card" to be set to "summary_large_image" to display the large-scale image, but (a) that's probably annoying to use, (b) probably wrong because it doesn't match Twitter Card's spec for a "photographic image", and (c) don't want to encourage/continue use of vendor-specific tag
  - [x] Verify cases with user avatar missing (or autogen), and repo avatar missing (falls back to repo owner avatar)

Pull request summary card:
![image](/attachments/b64283e3-9a3c-4f19-9d00-961662ffe86b)

Issue summary card:
![image](/attachments/318ce589-02e0-493e-b10c-5b2cb2627db2)

(images to the right are the custom repo avatar, w/ fallback to the repo owner avatar)

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.
  - OpenGraph capabilities are expected to work in the background without user awareness, and so there is no need for documentation to explain the capabilities for users.

### Release notes

- [ ] I do not want this change to show in the release notes.
- [x] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6053
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
2024-11-29 15:02:03 +00:00
..
actions Fix wrong status of Set up Job when first step is skipped (#32120) 2024-09-29 10:38:49 +02:00
activitypub test: fix test linting 2024-11-11 12:44:36 +01:00
analyze
assetfs Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
auth Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
avatar Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
base fix: extend forgejo_auth_token table 2024-11-15 10:59:36 +01:00
cache Increase cacheContextLifetime to reduce false reports (#32011) 2024-09-14 17:09:03 +02:00
card Add a "summary card" to issues & PRs for consumption by OpenGraph clients (#6053) 2024-11-29 15:02:03 +00:00
charset refactor: remove redundant err declarations (#32381) 2024-11-05 09:33:15 +01:00
container
csv Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
emoji
eventsource fix: use better code to group UID and stopwatches 2024-11-16 15:59:02 +01:00
forgefed style: reenable switch check 2024-08-18 15:19:01 +02:00
generate Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
git Fix submodule parsing 2024-11-24 11:00:06 +00:00
gitgraph fix: Proper paring of date for git commits 2024-11-10 22:23:28 +01:00
gitrepo [CHORE] Drop go-git support 2024-08-12 19:11:09 +02:00
graceful chore: remove some Windows-specific files 2024-09-19 20:39:55 +05:00
hcaptcha
highlight fix: normalize guessed languages from enry 2024-11-27 17:57:02 +01:00
hostmatcher Support allowed hosts for migrations to work with proxy (#32025) 2024-09-14 17:52:54 +02:00
html
httpcache Fix wrong last modify time (#32102) 2024-09-27 08:42:48 +02:00
httplib Fix wrong last modify time (#32102) 2024-09-27 08:42:48 +02:00
indexer test: add for issue indexer sort by score 2024-11-10 07:17:27 +00:00
issue/template [CHORE] Use forked binding library 2024-11-05 22:47:34 +01:00
json
keying feat: migrate TOTP secrets to keying 2024-11-27 00:34:16 +01:00
label
lfs test: fix test linting 2024-11-11 12:44:36 +01:00
log [CHORE] Fix darwin compatibility 2024-08-09 17:44:41 +02:00
markup Fix and refactor markdown rendering (#32522) 2024-11-17 20:34:28 +01:00
mcaptcha
metrics
migration Support migrating GitHub/GitLab PR draft status (#32242) 2024-10-20 09:24:25 +02:00
nosql [FEAT] Only implement used API of Redis client 2024-08-30 04:33:15 +02:00
optional Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
options
packages fix: handle renamed dependency for cargo registery 2024-11-13 21:26:49 +01:00
paginator
pprof
private fix(hook): ignore unknown push options instead of failing 2024-07-02 21:39:01 +02:00
process chore: remove some Windows-specific files 2024-09-19 20:39:55 +05:00
proxy
proxyprotocol
public
queue chore: update mock redis client 2024-09-01 05:42:34 +02:00
recaptcha
references Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
regexplru Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
repository disable gravatar in test (#32529) 2024-11-24 15:42:36 +00:00
secret Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
session [FEAT] Only implement used API of Redis client 2024-08-30 04:33:15 +02:00
setting Add Low German to list of default languages 2024-11-25 18:36:14 +01:00
sitemap Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
ssh
storage Add artifacts test fixture (#30300) 2024-11-05 09:33:15 +01:00
structs fix: Preview picture not visible on Markdown file (#5781) 2024-11-23 15:00:18 +00:00
svg
sync
system Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
templates [PORT] Refactor DateUtils and merge TimeSince (gitea#32409) 2024-11-10 22:23:27 +01:00
test
testlogger test: add trailing newline to testlogger.go:recordError message 2024-11-14 14:38:47 +01:00
timeutil [PORT] Refactor DateUtils and merge TimeSince (gitea#32409) 2024-11-10 22:23:27 +01:00
translation i18n: Add dummy language for checking translation keys (#5785) 2024-11-05 09:59:04 +00:00
turnstile
typesniffer feat: Make AVIF Images work with Forgejo 2024-11-13 19:09:40 +01:00
updatechecker Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
uri Add testifylint to lint checks (#4535) 2024-07-30 19:41:10 +00:00
user test: enforce tenv usage in tests 2024-07-17 23:07:41 +02:00
util test: fix test linting 2024-11-11 12:44:36 +01:00
validation [CHORE] Use forked binding library 2024-11-05 22:47:34 +01:00
web [CHORE] Use forked binding library 2024-11-05 22:47:34 +01:00
webhook Add support for workflow_dispatch (#3334) 2024-06-28 05:17:11 +00:00
zstd Cache generated binary across jobs 2024-08-26 23:43:09 +02:00