Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into starter-packs
Browse files Browse the repository at this point in the history
* origin/main:
  Add a11y context (#4586)
  center pill text in label pill (#4579)
  Wait for AppView when posting (#4584)
  Bsky link card service (#4547)
  • Loading branch information
estrattonbailey committed Jun 20, 2024
2 parents 4b8e5eb + 4bba597 commit 751c840
Show file tree
Hide file tree
Showing 22 changed files with 1,909 additions and 44 deletions.
55 changes: 55 additions & 0 deletions .github/workflows/build-and-push-ogcard-aws.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: build-and-push-ogcard-aws
on:
push:
branches:
- divy/bskycard

env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}
IMAGE_NAME: bskyogcard

jobs:
ogcard-container-aws:
if: github.repository == 'bluesky-social/social-app'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1

- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
password: ${{ env.PASSWORD }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
file: ./Dockerfile.bskyogcard
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
41 changes: 41 additions & 0 deletions Dockerfile.bskyogcard
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
FROM node:20.11-alpine3.18 as build

# Move files into the image and install
WORKDIR /app

COPY ./bskyogcard/package.json ./
COPY ./bskyogcard/yarn.lock ./
RUN yarn install --frozen-lockfile

COPY ./bskyogcard ./

# build then prune dev deps
RUN yarn build
RUN yarn install --production --ignore-scripts --prefer-offline

# Uses assets from build stage to reduce build size
FROM node:20.11-alpine3.18

RUN apk add --update dumb-init

# Avoid zombie processes, handle signal forwarding
ENTRYPOINT ["dumb-init", "--"]

WORKDIR /app
COPY --from=build /app /app
RUN mkdir /app/data && chown node /app/data

VOLUME /app/data
EXPOSE 3000
ENV CARD_PORT=3000
ENV NODE_ENV=production
# potential perf issues w/ io_uring on this version of node
ENV UV_USE_IO_URING=0

# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user
USER node
CMD ["node", "--heapsnapshot-signal=SIGUSR2", "--enable-source-maps", "dist/bin.js"]

LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app
LABEL org.opencontainers.image.description="Bsky Card Service"
LABEL org.opencontainers.image.licenses=UNLICENSED
24 changes: 24 additions & 0 deletions bskyogcard/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "bskyogcard",
"version": "0.0.0",
"type": "module",
"main": "src/index.ts",
"scripts": {
"start": "node --loader ts-node/esm ./src/bin.ts",
"build": "tsc && cp -r src/assets dist/assets"
},
"dependencies": {
"@atproto/api": "0.12.19-next.0",
"@atproto/common": "^0.4.0",
"@resvg/resvg-js": "^2.6.2",
"express": "^4.19.2",
"http-terminator": "^3.2.0",
"pino": "^9.2.0",
"react": "^18.3.1",
"satori": "^0.10.13"
},
"devDependencies": {
"@types/node": "^20.14.3",
"typescript": "^5.4.5"
}
}
Binary file added bskyogcard/src/assets/Inter-Bold.ttf
Binary file not shown.
48 changes: 48 additions & 0 deletions bskyogcard/src/bin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import cluster, {Worker} from 'node:cluster'

import {envInt} from '@atproto/common'

import {CardService, envToCfg, httpLogger, readEnv} from './index.js'

async function main() {
const env = readEnv()
const cfg = envToCfg(env)
const card = await CardService.create(cfg)
await card.start()
httpLogger.info('card service is running')
process.on('SIGTERM', async () => {
httpLogger.info('card service is stopping')
await card.destroy()
httpLogger.info('card service is stopped')
if (cluster.isWorker) process.exit(0)
})
}

const workerCount = envInt('CARD_CLUSTER_WORKER_COUNT')

if (workerCount) {
if (cluster.isPrimary) {
httpLogger.info(`primary ${process.pid} is running`)
const workers = new Set<Worker>()
for (let i = 0; i < workerCount; ++i) {
workers.add(cluster.fork())
}
let teardown = false
cluster.on('exit', worker => {
workers.delete(worker)
if (!teardown) {
workers.add(cluster.fork()) // restart on crash
}
})
process.on('SIGTERM', () => {
teardown = true
httpLogger.info('disconnecting workers')
workers.forEach(w => w.kill('SIGTERM'))
})
} else {
httpLogger.info(`worker ${process.pid} is running`)
main()
}
} else {
main() // non-clustering
}
16 changes: 16 additions & 0 deletions bskyogcard/src/components/Butterfly.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react'

export function Butterfly(props: React.SVGAttributes<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 568 501"
{...props}>
<path
fill="currentColor"
d="M123.121 33.664C188.241 82.553 258.281 181.68 284 234.873c25.719-53.192 95.759-152.32 160.879-201.21C491.866-1.611 568-28.906 568 57.947c0 17.346-9.945 145.713-15.778 166.555-20.275 72.453-94.155 90.933-159.875 79.748C507.222 323.8 536.444 388.56 473.333 453.32c-119.86 122.992-172.272-30.859-185.702-70.281-2.462-7.227-3.614-10.608-3.631-7.733-.017-2.875-1.169.506-3.631 7.733-13.43 39.422-65.842 193.273-185.702 70.281-63.111-64.76-33.89-129.52 80.986-149.071-65.72 11.185-139.6-7.295-159.875-79.748C9.945 203.659 0 75.291 0 57.946 0-28.906 76.135-1.612 123.121 33.664Z"
/>
</svg>
)
}
10 changes: 10 additions & 0 deletions bskyogcard/src/components/Img.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'

export function Img(
props: Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src'> & {src: Buffer},
) {
const {src, ...others} = props
return (
<img {...others} src={`data:image/jpeg;base64,${src.toString('base64')}`} />
)
}
149 changes: 149 additions & 0 deletions bskyogcard/src/components/StarterPack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/* eslint-disable bsky-internal/avoid-unwrapped-text */
import React from 'react'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'

import {Butterfly} from './Butterfly.js'
import {Img} from './Img.js'

export const STARTERPACK_HEIGHT = 630
export const STARTERPACK_WIDTH = 1200
export const TILE_SIZE = STARTERPACK_HEIGHT / 3

const GRADIENT_TOP = '#0A7AFF'
const GRADIENT_BOTTOM = '#59B9FF'
const IMAGE_STROKE = '#359CFF'

export function StarterPack(props: {
starterPack: AppBskyGraphDefs.StarterPackView
images: Map<string, Buffer>
}) {
const {starterPack, images} = props
const record = AppBskyGraphStarterpack.isRecord(starterPack.record)
? starterPack.record
: null
const imagesArray = [...images.values()]
const imageOfCreator = images.get(starterPack.creator.did)
const imagesExceptCreator = [...images.entries()]
.filter(([did]) => did !== starterPack.creator.did)
.map(([, image]) => image)
const imagesAcross: Buffer[] = []
if (imageOfCreator) {
if (imagesExceptCreator.length >= 6) {
imagesAcross.push(...imagesExceptCreator.slice(0, 3))
imagesAcross.push(imageOfCreator)
imagesAcross.push(...imagesExceptCreator.slice(3, 6))
} else {
const firstHalf = Math.floor(imagesExceptCreator.length / 2)
imagesAcross.push(...imagesExceptCreator.slice(0, firstHalf))
imagesAcross.push(imageOfCreator)
imagesAcross.push(
...imagesExceptCreator.slice(firstHalf, imagesExceptCreator.length),
)
}
} else {
imagesAcross.push(...imagesExceptCreator.slice(0, 7))
}
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
width: STARTERPACK_WIDTH,
height: STARTERPACK_HEIGHT,
backgroundColor: 'black',
color: 'white',
fontFamily: 'Inter',
}}>
{/* image tiles */}
<div
style={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'stretch',
width: TILE_SIZE * 6,
height: TILE_SIZE * 3,
}}>
{[...Array(18)].map((_, i) => {
const image = imagesArray.at(i % imagesArray.length)
return (
<div
key={i}
style={{
display: 'flex',
height: TILE_SIZE,
width: TILE_SIZE,
}}>
{image && <Img height="100%" width="100%" src={image} />}
</div>
)
})}
{/* background overlay */}
<div
style={{
display: 'flex',
width: '100%',
height: '100%',
position: 'absolute',
backgroundImage: `linear-gradient(to bottom, ${GRADIENT_TOP}, ${GRADIENT_BOTTOM})`,
opacity: 0.9,
}}
/>
</div>
{/* foreground text & images */}
<div
style={{
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
width: '100%',
height: '100%',
position: 'absolute',
color: 'white',
}}>
<div
style={{
color: 'white',
padding: 60,
fontSize: 40,
}}>
JOIN THE CONVERSATION
</div>
<div style={{display: 'flex'}}>
{imagesAcross.map((image, i) => {
return (
<div
key={i}
style={{
display: 'flex',
height: 172 + 15 * 2,
width: 172 + 15 * 2,
margin: -15,
border: `15px solid ${IMAGE_STROKE}`,
borderRadius: '50%',
overflow: 'hidden',
}}>
<Img height="100%" width="100%" src={image} />
</div>
)
})}
</div>
<div
style={{
padding: '75px 30px 0px',
fontSize: 65,
}}>
{record?.name || 'Starter Pack'}
</div>
<div
style={{
display: 'flex',
fontSize: 40,
justifyContent: 'center',
padding: '30px 30px 10px',
}}>
on <Butterfly width="65" style={{margin: '-7px 10px 0'}} /> Bluesky
</div>
</div>
</div>
)
}
40 changes: 40 additions & 0 deletions bskyogcard/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {envInt, envStr} from '@atproto/common'

export type Config = {
service: ServiceConfig
}

export type ServiceConfig = {
port: number
version?: string
appviewUrl: string
originVerify?: string
}

export type Environment = {
port?: number
version?: string
appviewUrl?: string
originVerify?: string
}

export const readEnv = (): Environment => {
return {
port: envInt('CARD_PORT'),
version: envStr('CARD_VERSION'),
appviewUrl: envStr('CARD_APPVIEW_URL'),
originVerify: envStr('CARD_ORIGIN_VERIFY'),
}
}

export const envToCfg = (env: Environment): Config => {
const serviceCfg: ServiceConfig = {
port: env.port ?? 3000,
version: env.version,
appviewUrl: env.appviewUrl ?? 'https://api.bsky.app',
originVerify: env.originVerify,
}
return {
service: serviceCfg,
}
}
Loading

0 comments on commit 751c840

Please sign in to comment.