Skip to content

Commit 6656221

Browse files
committed
feature: add bluesky post bot
1 parent 2e68282 commit 6656221

10 files changed

Lines changed: 210 additions & 51 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- uses: pnpm/action-setup@v4
2828
name: Install pnpm
2929
with:
30-
version: 9
30+
version: 10
3131
run_install: false
3232

3333
- name: Get pnpm store directory

.github/workflows/preview.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- uses: pnpm/action-setup@v4
2323
name: Install pnpm
2424
with:
25-
version: 9
25+
version: 10
2626
run_install: false
2727

2828
- name: Get pnpm store directory
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
name: Tweet
1+
name: Socials
22

33
on:
44
schedule:
55
- cron: '0 6 * * *'
66
# workflow_dispatch:
77

88
jobs:
9-
deploy:
9+
post:
1010
runs-on: ubuntu-latest
1111
steps:
1212
- name: Checkout
@@ -20,7 +20,7 @@ jobs:
2020
- uses: pnpm/action-setup@v4
2121
name: Install pnpm
2222
with:
23-
version: 9
23+
version: 10
2424
run_install: false
2525

2626
- name: Get pnpm store directory
@@ -55,3 +55,9 @@ jobs:
5555
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
5656
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
5757
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
58+
59+
- name: Bluesky
60+
run: pnpm run bluesky
61+
env:
62+
BLUESKY_USERNAME: ${{ secrets.BLUESKY_USERNAME }}
63+
BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }}

@package/scrapers/src/entities.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ export type Lecture = t.TypeOf<typeof Lecture>
2222
// Hosts
2323
export const Host = t.object({
2424
name: t.string(),
25-
website: t.string(),
2625
icon: t.string().optional(),
27-
twitter: t.string().optional(),
28-
threads: t.string().optional(),
26+
website: t.string(),
2927
description: t.string().optional(),
3028
lectures: t.array(Lecture),
29+
twitter: t.string().optional(),
30+
threads: t.string().optional(),
31+
bluesky: t.string().optional(),
3132
})
3233

3334
export type Host = t.TypeOf<typeof Host>

@package/scripts/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"license": "Private",
66
"author": "Dan Beaven <dm.beaven@gmail.com>",
77
"bin": {
8-
"script-artifact": "lib/artifact.js",
8+
"script-bluesky": "lib/bluesky.js",
99
"script-collect": "lib/collect.js",
1010
"script-tweet": "lib/tweet.js"
1111
},
@@ -19,6 +19,7 @@
1919
"dependencies": {
2020
"@actions/core": "^1.10.1",
2121
"@actions/github": "^6.0.0",
22+
"@atproto/api": "^0.15.0",
2223
"@package/scrapers": "workspace:^",
2324
"cmd-ts": "^0.13.0",
2425
"dayjs": "^1.11.13",

@package/scripts/src/bluesky.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env node
2+
3+
import { command, run, string, positional } from 'cmd-ts'
4+
import { AtpAgent } from '@atproto/api'
5+
import dayjs from 'dayjs'
6+
7+
import { siteLink, getTodaysLectures } from './util'
8+
9+
process.env.TZ = 'Europe/London'
10+
11+
const app = command({
12+
name: 'bluesky',
13+
args: {
14+
lectures: positional({ type: string, displayName: 'Json file containing lectures' }),
15+
},
16+
handler: async ({ lectures }) => {
17+
const agent = new AtpAgent({ service: 'https://bsky.social' })
18+
19+
await agent.login({ identifier: process.env.BLUESKY_USERNAME!, password: process.env.BLUESKY_PASSWORD! })
20+
21+
const data = await getTodaysLectures(lectures)
22+
23+
for (let talk of data.slice(0, 1)) {
24+
const link = siteLink(talk)
25+
let text = `
26+
${talk.title}
27+
28+
${dayjs(talk.time_start).format('HH:mm')}: ${talk.host.name}
29+
30+
${link}
31+
`.trim()
32+
33+
const linkStart = text.indexOf(link)
34+
const linkEnd = linkStart + link.length
35+
36+
await agent.post({
37+
text: text,
38+
tags: ['lectureslondon', 'london', 'lectures', 'publiclectures'],
39+
facets: [
40+
{
41+
index: { byteStart: linkStart, byteEnd: linkEnd },
42+
features: [{ uri: link, $type: 'app.bsky.richtext.facet#link' }],
43+
},
44+
],
45+
})
46+
}
47+
},
48+
})
49+
50+
run(app, process.argv.slice(2))

@package/scripts/src/tweet.ts

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import { command, run, string, positional } from 'cmd-ts'
44
import { TwitterApi } from 'twitter-api-v2'
55
import dayjs from 'dayjs'
6-
import fs from 'fs'
76
import z from 'zod'
87

8+
import { getTodaysLectures, siteLink } from './util'
9+
910
process.env.TZ = 'Europe/London'
1011

1112
const app = command({
@@ -26,19 +27,19 @@ const app = command({
2627
return Promise.reject(new Error('Missing twitter client keys'))
2728
})
2829

29-
const data = await fs.promises
30-
.readFile(lectures, 'utf-8')
31-
.then((x) => JSON.parse(x))
32-
.then((x) => (x as unknown[]).filter((y): y is LectureSchema => LectureSchema.safeParse(y).success))
33-
.then((x) =>
34-
x.filter(
35-
(y) =>
36-
dayjs(y.time_start).isAfter(dayjs().startOf('day')) && dayjs(y.time_start).isBefore(dayjs().endOf('day')),
37-
),
38-
)
30+
const data = await getTodaysLectures(lectures)
31+
32+
for (let talk of data) {
33+
const status = `
34+
${talk.title}
35+
${dayjs(talk.time_start).format('HH:mm')}: ${talk.host.name}
36+
${siteLink(talk)}
37+
38+
${talk.link}
39+
40+
${talk.host.twitter} #london #lectures #publiclectures #lectureslondon
41+
`.trim()
3942

40-
for (let i in data) {
41-
const status = generateLectureTweet(data[i])
4243
await client.v2.tweet(status).catch((e) => {
4344
console.error(`Failed to post`, e)
4445
console.error(status)
@@ -48,34 +49,9 @@ const app = command({
4849
})
4950
run(app, process.argv.slice(2))
5051

51-
const LectureSchema = z.object({
52-
id: z.string(),
53-
link: z.string(),
54-
title: z.string(),
55-
time_start: z.string(),
56-
host: z.object({ name: z.string(), twitter: z.string() }),
57-
})
58-
type LectureSchema = z.infer<typeof LectureSchema>
59-
6052
const TwitSchema = z.object({
6153
appKey: z.string(),
6254
appSecret: z.string(),
6355
accessToken: z.string(),
6456
accessSecret: z.string(),
6557
})
66-
67-
const generateLectureTweet = (talk: z.TypeOf<typeof LectureSchema>) =>
68-
`${talk.title}
69-
${dayjs(talk.time_start).format('HH:mm')}: ${talk.host.name}
70-
https://lectures.london/${slugit(talk.host.name)}/${slugit(talk.title)}
71-
72-
${talk.link}
73-
74-
${talk.host.twitter} #london #lectures #publiclectures #lectureslondon
75-
`
76-
77-
const slugit = (str: string) =>
78-
str
79-
.replace(/[^\w\s]/gim, '')
80-
.replace(/\s{1,}/gim, '-')
81-
.toLowerCase()

@package/scripts/src/util/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import dayjs from 'dayjs'
2+
import fs from 'fs'
3+
import z from 'zod'
4+
5+
export const getTodaysLectures = async (file: string) => {
6+
return fs.promises
7+
.readFile(file, 'utf-8')
8+
.then((x) => JSON.parse(x))
9+
.then((x) => (x as unknown[]).filter((y): y is LectureSchema => LectureSchema.safeParse(y).success))
10+
.then((x) =>
11+
x.filter(
12+
(y) =>
13+
dayjs(y.time_start).isAfter(dayjs().startOf('day')) && dayjs(y.time_start).isBefore(dayjs().endOf('day')),
14+
),
15+
)
16+
}
17+
18+
export const LectureSchema = z.object({
19+
id: z.string(),
20+
link: z.string(),
21+
title: z.string(),
22+
time_start: z.string(),
23+
host: z.object({ name: z.string(), twitter: z.string(), bluesky: z.string().optional() }),
24+
})
25+
export type LectureSchema = z.infer<typeof LectureSchema>
26+
27+
export const siteLink = (talk: LectureSchema) =>
28+
`https://lectures.london/${slugit(talk.host.name)}/${slugit(talk.title)}`
29+
30+
const slugit = (str: string) =>
31+
str
32+
.replace(/[^\w\s]/gim, '')
33+
.replace(/\s{1,}/gim, '-')
34+
.toLowerCase()

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"collect": "script-collect -l ./@client/lectures.json --hosts ./@client/hosts.json",
1515
"artifact": "script-artifact --name $ARTIFACT_NAME --token $GITHUB_TOKEN --owner Pingid --repo lectures-london --dest '@client'",
1616
"tweet": "pnpm script-tweet './@client/lectures.json'",
17+
"bluesky": "pnpm script-bluesky './@client/lectures.json'",
1718
"format": "prettier --write \"**/*.{ts,tsx,md,json}\"",
1819
"clean:ts": "tsc -b --clean",
1920
"watch:ts": "tsc -b --watch"
@@ -34,6 +35,12 @@
3435
"pnpm": {
3536
"patchedDependencies": {
3637
"unfluffjs": "patches/unfluffjs.patch"
37-
}
38+
},
39+
"onlyBuiltDependencies": [
40+
"esbuild",
41+
"netlify-cli",
42+
"sharp",
43+
"unix-dgram"
44+
]
3845
}
3946
}

0 commit comments

Comments
 (0)