Bulletin Board — Board Data & the Post Feed
Overview
Two read endpoints for the board: board-level data (settings, the OA's identity, and the categories the caller is allowed to see) and a keyset-paginated post feed complete with a pinned strip and the caller's reaction state already decorated onto each card.
The board is deliberately pseudonymous — PostView carries no author name or picture at all. The only identity returned is the OA's, used to render CMS-authored posts as coming "from the page". Since that value is identical for every post, it is sent once alongside the board data rather than repeated on every card.
Business Flow
GET /api/bulletin/:hash
- Call
resolveAccess, then checkCanView()— failure returns 403. - Load the board's categories and filter them through
CanSeeCategoryon the way out.findCategoriesSQLfilters only on board, status, and deleted_date; it knows nothing aboutaccess_mode. Without this filter, guests and members outside the audience could read the names and audience ids of restricted categories. - Return
{boardId, settings, oaName, oaPictureUrl, categories}.oaPictureUrlresolvesline_oa.coverto a public URL and is an empty string when absent — never a half-formed URL — so clients can fall back on a plain falsy check.
GET /api/bulletin/:hash/posts
Query parameters: category (optional int64), cursor (optional, opaque), and limit (default 20, capped at 50 by the repository).
- Call
resolveAccess, then checkCanView(). - An unparseable
categoryreturns 400 withinvalid category. A malformed cursor is deliberately accepted as "start from the top" rather than returning 400. - The pinned strip is attached only to the first unfiltered page — when both
categoryIDandcursorare absent — because the strip is not paginated and attaching it to every page would duplicate content. It is capped at five posts by the package-level constantmaxPinnedPosts, which is not a setting; the CMS pin endpoint already rejects anything beyond that number. - Fetch one page of the feed with
FindFeed(boardID, accessCtx, categoryID, cursor, limit). The SQL encodes the visibility rules directly:publishedposts are visible if the caller can see their category,pendingposts are visible only to their owner, andhiddenanddeletedposts are visible to nobody. - Cursor — a page that fills the limit returns a
nextCursor, the base64 encoding of"unixMilli:id"for the last row, keyed onlast_activity_datebecause that is the column the feed actually orders by. Usingcreated_dateinstead would cause page two to both duplicate and skip rows. The cursor is opaque by design: a client that was not handed one cannot fabricate it. - Decorate reactions in a single batch covering both the pinned strip and the feed, via
ReactionStates(targetType, ids, lineOaID, lineUserID)followed byDecoratePostViews. Every card therefore arrives already knowing the caller's own reaction, with no per-card query. - Return
{pinned, posts, nextCursor}, wherenextCursoris present only when a further page exists.
PostView and CommentView
Both are projections that read author_line_user_id solely to compute isMine, and never copy that value anywhere else. The images field is decoded from jsonb into an array of keys.
Key Files & Functions
| Route | Handler |
|---|---|
GET /api/bulletin/:hash | internal/bulletin/handler.go → (*Handler).GetBoard |
GET /api/bulletin/:hash/posts | (*Handler).ListPosts |
internal/bulletin/repository.go—FindPinned,FindFeed,ReactionStates,FindCategories,categoryVisibilitySQL,filterByCategoryinternal/bulletin/view.go—PostView,NewPostView,NewPostViews,DecoratePostViews,CategoryView,visibleCategoryViews,decodeImages,ownedBy,publicURLOfinternal/bulletin/entity.go—Post,Category,ReactionState,FeedCursor,EncodeCursor,DecodeCursor,maxPinnedPosts- Internal response types —
boardView,feedView
Connections to Other Services
- Database — tables
bulletin.board,bulletin.category,bulletin.post,bulletin.reaction, andline_oa - Object storage —
internal/storagexresolvesline_oa.coverto a public URL, accepting nil and yielding an empty string - Bulletin access control — uses
resolveAccess,CanView, andCanSeeCategory - App-enabled guard — governs whether the feature group is switched on at the platform level
- client-web — corresponds to the
bulletin-boardfeature