chore: migrated to forgejo
This commit is contained in:
parent
082d230cd8
commit
de60045100
9 changed files with 207 additions and 94 deletions
|
@ -5,6 +5,7 @@ on:
|
|||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
concurrency:
|
||||
group: deploy
|
||||
cancel-in-progress: true
|
||||
|
@ -12,7 +13,7 @@ concurrency:
|
|||
jobs:
|
||||
publish:
|
||||
if: github.repository == 'teidesu/tei.su' # do not run on forks
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: docker-dind
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
@ -29,7 +30,7 @@ jobs:
|
|||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
registry: git.stupid.fish
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
@ -37,7 +38,7 @@ jobs:
|
|||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/teidesu/tei.su
|
||||
images: git.stupid.fish/teidesu/tei.su
|
||||
tags: type=sha
|
||||
flavor: latest=true
|
||||
- name: Build and push
|
||||
|
@ -50,7 +51,7 @@ jobs:
|
|||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: node22
|
||||
needs: publish
|
||||
steps:
|
||||
- uses: teidesu/desu-deploy@main
|
|
@ -7,6 +7,7 @@ export default antfu({
|
|||
typescript: true,
|
||||
astro: true,
|
||||
solid: true,
|
||||
yaml: false,
|
||||
rules: {
|
||||
'curly': ['error', 'multi-line'],
|
||||
'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
||||
|
|
|
@ -3,6 +3,8 @@ import { z } from 'zod'
|
|||
|
||||
import { ffetch } from '../../utils/fetch.ts'
|
||||
|
||||
import type { LastSeenItem } from './index.ts'
|
||||
|
||||
const ENDPOINT = 'https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed'
|
||||
const TTL = 3 * 60 * 60 * 1000 // 3 hours
|
||||
const STALE_TTL = 8 * 60 * 60 * 1000 // 8 hours
|
||||
|
@ -15,7 +17,7 @@ const schema = z.object({
|
|||
}),
|
||||
})
|
||||
|
||||
export const bskyLastSeen = new AsyncResource<z.infer<typeof schema>>({
|
||||
export const bskyLastSeen = new AsyncResource<LastSeenItem | null>({
|
||||
async fetcher() {
|
||||
const res = await ffetch(ENDPOINT, {
|
||||
query: {
|
||||
|
@ -29,8 +31,24 @@ export const bskyLastSeen = new AsyncResource<z.infer<typeof schema>>({
|
|||
})),
|
||||
}))
|
||||
|
||||
const post = res.feed[0].post
|
||||
|
||||
const postId = post.uri.match(/at:\/\/did:web:tei.su\/app\.bsky\.feed\.post\/([a-zA-Z0-9]+)/)
|
||||
if (postId) {
|
||||
return {
|
||||
data: {
|
||||
source: 'bsky',
|
||||
sourceLink: 'https://bsky.app/profile/did:web:tei.su',
|
||||
time: new Date(post.record.createdAt).getTime(),
|
||||
text: post.record.text.slice(0, 40) || '[no text]',
|
||||
link: `https://bsky.app/profile/did:web:tei.su/post/${postId[1]}`,
|
||||
},
|
||||
expiresIn: TTL,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: res.feed[0].post,
|
||||
data: null,
|
||||
expiresIn: TTL,
|
||||
}
|
||||
},
|
||||
|
|
80
src/backend/service/last-seen/forgejo.ts
Normal file
80
src/backend/service/last-seen/forgejo.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { AsyncResource } from '@fuman/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ffetch } from '../../utils/fetch.ts'
|
||||
|
||||
import type { LastSeenItem } from './index.ts'
|
||||
|
||||
const ENDPOINT = 'https://git.stupid.fish/api/v1/users/teidesu/activities/feeds?only-performed-by=true'
|
||||
const TTL = 1 * 60 * 60 * 1000 // 1 hour
|
||||
const STALE_TTL = 4 * 60 * 60 * 1000 // 4 hours
|
||||
|
||||
const schema = z.object({
|
||||
id: z.number(),
|
||||
// create_repo,rename_repo,star_repo,watch_repo,commit_repo,create_issue,create_pull_request,transfer_repo,push_tag,comment_issue,
|
||||
// merge_pull_request,close_issue,reopen_issue,close_pull_request,reopen_pull_request,delete_tag,delete_branch,mirror_sync_push,
|
||||
// mirror_sync_create,mirror_sync_delete,approve_pull_request,reject_pull_request,comment_pull,publish_release,pull_review_dismissed,
|
||||
// pull_request_ready_for_review,auto_merge_pull_request
|
||||
op_type: z.string(),
|
||||
content: z.string(),
|
||||
repo: z.object({
|
||||
full_name: z.string(),
|
||||
html_url: z.string(),
|
||||
}),
|
||||
created: z.string(),
|
||||
})
|
||||
|
||||
// {"Commits":[{"Sha1":"2ee71666823761350bd28a0db9ef9140cd3814cb","Message":"docs: fix docs config\n","AuthorEmail":"alina@tei.su","AuthorName":"alina sireneva","CommitterEmail":"alina@tei.su","CommitterName":"alina sireneva","Timestamp":"2025-01-03T22:42:30+03:00"}],"HeadCommit":{"Sha1":"2ee71666823761350bd28a0db9ef9140cd3814cb","Message":"docs: fix docs config\n","AuthorEmail":"alina@tei.su","AuthorName":"alina sireneva","CommitterEmail":"alina@tei.su","CommitterName":"alina sireneva","Timestamp":"2025-01-03T22:42:30+03:00"},"CompareURL":"teidesu/mtcute/compare/db9b083d3595e78240146e8a590fe70049259612...2ee71666823761350bd28a0db9ef9140cd3814cb","Len":1}
|
||||
const CommitEventSchema = z.object({
|
||||
Commits: z.array(z.object({
|
||||
Message: z.string(),
|
||||
})),
|
||||
})
|
||||
|
||||
function mkItem(item: z.infer<typeof schema>, text: string) {
|
||||
return {
|
||||
source: 'forgejo',
|
||||
sourceLink: 'https://git.stupid.fish/teidesu',
|
||||
time: new Date(item.created).getTime(),
|
||||
text: item.repo.full_name,
|
||||
link: item.repo.html_url,
|
||||
suffix: `: ${text}`,
|
||||
}
|
||||
}
|
||||
|
||||
export const forgejoLastSeen = new AsyncResource<LastSeenItem | null>({
|
||||
async fetcher() {
|
||||
const res = await ffetch(ENDPOINT).parsedJson(z.array(schema))
|
||||
|
||||
// for simplicity (and lack of proper documentation) we'll just support a few common events and return the first supported one
|
||||
|
||||
let result: LastSeenItem | null = null
|
||||
|
||||
for (const item of res) {
|
||||
if (item.op_type === 'commit_repo') {
|
||||
const commits = CommitEventSchema.parse(JSON.parse(item.content)).Commits
|
||||
|
||||
result = mkItem(
|
||||
item,
|
||||
commits.length === 1 && commits[0].Message.length > 0
|
||||
? `${commits[0].Message.slice(0, 40)}`
|
||||
: `pushed ${commits.length} commits`,
|
||||
)
|
||||
break
|
||||
} else if (item.op_type === 'close_pull_request') {
|
||||
result = mkItem(item, 'closed pull request')
|
||||
break
|
||||
} else if (item.op_type === 'merge_pull_request') {
|
||||
result = mkItem(item, 'merged pull request')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: result,
|
||||
expiresIn: TTL,
|
||||
}
|
||||
},
|
||||
swr: true,
|
||||
swrValidator: ({ currentFetchedAt }) => Date.now() - currentFetchedAt < STALE_TTL,
|
||||
})
|
|
@ -3,6 +3,8 @@ import { z } from 'zod'
|
|||
|
||||
import { ffetch } from '../../utils/fetch.ts'
|
||||
|
||||
import type { LastSeenItem } from './index.ts'
|
||||
|
||||
const ENDPOINT = 'https://api.github.com/users/teidesu/events/public?per_page=1'
|
||||
const TTL = 1 * 60 * 60 * 1000 // 1 hour
|
||||
const STALE_TTL = 4 * 60 * 60 * 1000 // 4 hours
|
||||
|
@ -16,7 +18,7 @@ const schema = z.object({
|
|||
created_at: z.string(),
|
||||
})
|
||||
|
||||
export const githubLastSeen = new AsyncResource<z.infer<typeof schema>>({
|
||||
export const githubLastSeen = new AsyncResource<LastSeenItem | null>({
|
||||
async fetcher() {
|
||||
const res = await ffetch(ENDPOINT, {
|
||||
headers: {
|
||||
|
@ -25,8 +27,38 @@ export const githubLastSeen = new AsyncResource<z.infer<typeof schema>>({
|
|||
},
|
||||
}).parsedJson(z.array(schema))
|
||||
|
||||
const data = res[0]
|
||||
|
||||
const eventTextMapper: Record<string, () => string> = {
|
||||
CreateEvent: () => `${data.payload.ref_type} created`,
|
||||
DeleteEvent: () => `${data.payload.ref_type} deleted`,
|
||||
ForkEvent: () => 'forked',
|
||||
GollumEvent: () => 'wiki updated',
|
||||
IssueCommentEvent: () => `issue comment ${data.payload.action}`,
|
||||
IssuesEvent: () => `issue ${data.payload.action}`,
|
||||
PublicEvent: () => 'made public',
|
||||
PullRequestEvent: () => `pr ${data.payload.action}`,
|
||||
PushEvent: () => `pushed ${data.payload.distinct_size} commits`,
|
||||
ReleaseEvent: () => `release ${data.payload.action}`,
|
||||
WatchEvent: () => 'starred',
|
||||
}
|
||||
|
||||
if (eventTextMapper[data.type]) {
|
||||
return {
|
||||
data: {
|
||||
source: 'github',
|
||||
sourceLink: 'https://github.com/teidesu',
|
||||
time: new Date(data.created_at).getTime(),
|
||||
text: data.repo.name,
|
||||
suffix: `: ${eventTextMapper[data.type]()}`,
|
||||
link: `https://github.com/${data.repo.name}`,
|
||||
},
|
||||
expiresIn: TTL,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: res[0],
|
||||
data: null,
|
||||
expiresIn: TTL,
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@ import { bskyLastSeen } from './bsky.ts'
|
|||
import { githubLastSeen } from './github'
|
||||
import { lastfm } from './lastfm'
|
||||
import { shikimoriLastSeen } from './shikimori'
|
||||
import { forgejoLastSeen } from './forgejo.ts'
|
||||
|
||||
export interface LastSeenItem {
|
||||
source: string
|
||||
|
@ -20,93 +21,33 @@ export async function fetchLastSeen() {
|
|||
bskyData,
|
||||
shikimoriData,
|
||||
githubData,
|
||||
forgejoData,
|
||||
] = await Promise.all([
|
||||
lastfm.get(),
|
||||
bskyLastSeen.get(),
|
||||
shikimoriLastSeen.get(),
|
||||
githubLastSeen.get(),
|
||||
forgejoLastSeen.get(),
|
||||
])
|
||||
|
||||
const res: LastSeenItem[] = []
|
||||
|
||||
if (lastfmData) {
|
||||
res.push({
|
||||
source: 'last.fm',
|
||||
sourceLink: 'https://last.fm/user/teidesu',
|
||||
time: Number(lastfmData.date!.uts) * 1000,
|
||||
text: `${lastfmData.name} – ${lastfmData.artist['#text']}`,
|
||||
link: lastfmData.url,
|
||||
})
|
||||
if (lastfmData) res.push(lastfmData)
|
||||
if (bskyData) res.push(bskyData)
|
||||
if (shikimoriData) res.push(shikimoriData)
|
||||
|
||||
if (githubData && forgejoData) {
|
||||
// only push the last one
|
||||
if (forgejoData.time > githubData.time) {
|
||||
res.push(forgejoData)
|
||||
} else {
|
||||
res.push(githubData)
|
||||
}
|
||||
} else if (githubData) {
|
||||
res.push(githubData)
|
||||
} else if (forgejoData) {
|
||||
res.push(forgejoData)
|
||||
}
|
||||
|
||||
if (bskyData) {
|
||||
const postId = bskyData.uri.match(/at:\/\/did:web:tei.su\/app\.bsky\.feed\.post\/([a-zA-Z0-9]+)/)
|
||||
if (postId) {
|
||||
res.push({
|
||||
source: 'bsky',
|
||||
sourceLink: 'https://bsky.app/profile/did:web:tei.su',
|
||||
time: new Date(bskyData.record.createdAt).getTime(),
|
||||
text: bskyData.record.text.slice(0, 40) || '[no text]',
|
||||
link: `https://bsky.app/profile/did:web:tei.su/post/${postId[1]}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (shikimoriData) {
|
||||
// thx morr for this fucking awesome api
|
||||
|
||||
const mapper: Record<string, string> = {
|
||||
'Просмотрено': 'completed',
|
||||
'Прочитано': 'completed',
|
||||
'Добавлено в список': 'added',
|
||||
'Брошено': 'dropped',
|
||||
}
|
||||
let event = mapper[shikimoriData.description]
|
||||
|
||||
if (!event && shikimoriData.description.match(/^Просмотрен.*эпизод(ов)?$/)) {
|
||||
event = 'watched'
|
||||
}
|
||||
if (!event && shikimoriData.description.match(/^(Просмотрено|Прочитано) и оценено/)) {
|
||||
event = 'completed'
|
||||
}
|
||||
|
||||
if (event) {
|
||||
res.push({
|
||||
source: 'shiki',
|
||||
sourceLink: 'https://shikimori.one/teidesu',
|
||||
time: new Date(shikimoriData.created_at).getTime(),
|
||||
text: shikimoriData.target.name,
|
||||
suffix: `: ${event}`,
|
||||
link: `https://shikimori.one${shikimoriData.target.url}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (githubData) {
|
||||
const eventTextMapper: Record<string, () => string> = {
|
||||
CreateEvent: () => `${githubData.payload.ref_type} created`,
|
||||
DeleteEvent: () => `${githubData.payload.ref_type} deleted`,
|
||||
ForkEvent: () => 'forked',
|
||||
GollumEvent: () => 'wiki updated',
|
||||
IssueCommentEvent: () => `issue comment ${githubData.payload.action}`,
|
||||
IssuesEvent: () => `issue ${githubData.payload.action}`,
|
||||
PublicEvent: () => 'made public',
|
||||
PullRequestEvent: () => `pr ${githubData.payload.action}`,
|
||||
PushEvent: () => `pushed ${githubData.payload.distinct_size} commits`,
|
||||
ReleaseEvent: () => `release ${githubData.payload.action}`,
|
||||
WatchEvent: () => 'starred',
|
||||
}
|
||||
if (eventTextMapper[githubData.type]) {
|
||||
res.push({
|
||||
source: 'github',
|
||||
sourceLink: 'https://github.com/teidesu',
|
||||
time: new Date(githubData.created_at).getTime(),
|
||||
text: githubData.repo.name,
|
||||
suffix: `: ${eventTextMapper[githubData.type]()}`,
|
||||
link: `https://github.com/${githubData.repo.name}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return res.sort((a, b) => b.time - a.time)
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import { zodValidate } from '~/utils/zod'
|
|||
import { env } from '~/backend/env'
|
||||
import { ffetch } from '../../utils/fetch.ts'
|
||||
|
||||
import type { LastSeenItem } from './index.ts'
|
||||
|
||||
const LASTFM_TTL = 1000 * 60 * 5 // 5 minutes
|
||||
const LASTFM_STALE_TTL = 1000 * 60 * 60 // 1 hour
|
||||
const LASTFM_USERNAME = 'teidesu'
|
||||
|
@ -19,7 +21,6 @@ const LastfmTrack = z.object({
|
|||
nowplaying: z.literal('true'),
|
||||
}).partial().optional(),
|
||||
})
|
||||
export type LastfmTrack = z.infer<typeof LastfmTrack>
|
||||
|
||||
const ResponseSchema = z.object({
|
||||
recenttracks: z.object({
|
||||
|
@ -34,7 +35,7 @@ const ResponseSchema = z.object({
|
|||
}),
|
||||
})
|
||||
|
||||
export const lastfm = new AsyncResource<LastfmTrack>({
|
||||
export const lastfm = new AsyncResource<LastSeenItem | null>({
|
||||
async fetcher({ current }) {
|
||||
const res = await ffetch('https://ws.audioscrobbler.com/2.0/', {
|
||||
query: {
|
||||
|
@ -43,7 +44,7 @@ export const lastfm = new AsyncResource<LastfmTrack>({
|
|||
api_key: LASTFM_TOKEN,
|
||||
format: 'json',
|
||||
limit: '1',
|
||||
from: current?.date?.uts,
|
||||
from: current ? Math.floor(current.time / 1000) : undefined,
|
||||
},
|
||||
}).parsedJson(ResponseSchema)
|
||||
|
||||
|
@ -55,7 +56,13 @@ export const lastfm = new AsyncResource<LastfmTrack>({
|
|||
}
|
||||
|
||||
return {
|
||||
data: track,
|
||||
data: {
|
||||
source: 'last.fm',
|
||||
sourceLink: 'https://last.fm/user/teidesu',
|
||||
time: Number(track.date!.uts) * 1000,
|
||||
text: `${track.name} – ${track.artist['#text']}`,
|
||||
link: track.url,
|
||||
},
|
||||
expiresIn: LASTFM_TTL,
|
||||
}
|
||||
},
|
||||
|
|
|
@ -3,6 +3,8 @@ import { z } from 'zod'
|
|||
|
||||
import { ffetch } from '../../utils/fetch.ts'
|
||||
|
||||
import type { LastSeenItem } from './index.ts'
|
||||
|
||||
const ENDPOINT = 'https://shikimori.one/api/users/698215/history?limit=1'
|
||||
const TTL = 3 * 60 * 60 * 1000 // 3 hours
|
||||
const STALE_TTL = 8 * 60 * 60 * 1000 // 8 hours
|
||||
|
@ -16,12 +18,43 @@ const schema = z.object({
|
|||
}),
|
||||
})
|
||||
|
||||
export const shikimoriLastSeen = new AsyncResource<z.infer<typeof schema>>({
|
||||
export const shikimoriLastSeen = new AsyncResource<LastSeenItem | null>({
|
||||
async fetcher() {
|
||||
const res = await ffetch(ENDPOINT).parsedJson(z.array(schema))
|
||||
const res = (await ffetch(ENDPOINT).parsedJson(z.array(schema)))[0]
|
||||
|
||||
// thx morr for this fucking awesome api
|
||||
|
||||
const mapper: Record<string, string> = {
|
||||
'Просмотрено': 'completed',
|
||||
'Прочитано': 'completed',
|
||||
'Добавлено в список': 'added',
|
||||
'Брошено': 'dropped',
|
||||
}
|
||||
let event = mapper[res.description]
|
||||
|
||||
if (!event && res.description.match(/^Просмотрен.*эпизод(ов)?$/)) {
|
||||
event = 'watched'
|
||||
}
|
||||
if (!event && res.description.match(/^(Просмотрено|Прочитано) и оценено/)) {
|
||||
event = 'completed'
|
||||
}
|
||||
|
||||
if (event) {
|
||||
return {
|
||||
data: {
|
||||
source: 'shiki',
|
||||
sourceLink: 'https://shikimori.one/teidesu',
|
||||
time: new Date(res.created_at).getTime(),
|
||||
text: res.target.name,
|
||||
suffix: `: ${event}`,
|
||||
link: `https://shikimori.one${res.target.url}`,
|
||||
},
|
||||
expiresIn: TTL,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: res[0],
|
||||
data: null,
|
||||
expiresIn: TTL,
|
||||
}
|
||||
},
|
||||
|
|
|
@ -19,7 +19,7 @@ import Header from './Header.astro'
|
|||
{' '}
|
||||
{import.meta.env.VITE_BUILD_DATE}
|
||||
{' / '}
|
||||
<Link href="//github.com/teidesu/tei.su" target="_blank">
|
||||
<Link href="//git.stupid.fish/teidesu/tei.su" target="_blank">
|
||||
source code
|
||||
</Link>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue