From 87dface848900ecb1aeff711fc5de20e4d19e603 Mon Sep 17 00:00:00 2001 From: alina sireneva Date: Sat, 4 Jan 2025 02:50:49 +0300 Subject: [PATCH] chore: migrated to forgejo --- {.github => .forgejo}/workflows/publish.yaml | 12 ++- eslint.config.js | 1 + src/backend/service/last-seen/bsky.ts | 22 ++++- src/backend/service/last-seen/forgejo.ts | 80 +++++++++++++++ src/backend/service/last-seen/github.ts | 36 ++++++- src/backend/service/last-seen/index.ts | 97 ++++--------------- src/backend/service/last-seen/lastfm.ts | 15 ++- src/backend/service/last-seen/shikimori.ts | 39 +++++++- src/layouts/DefaultLayout/DefaultLayout.astro | 2 +- 9 files changed, 210 insertions(+), 94 deletions(-) rename {.github => .forgejo}/workflows/publish.yaml (88%) create mode 100644 src/backend/service/last-seen/forgejo.ts diff --git a/.github/workflows/publish.yaml b/.forgejo/workflows/publish.yaml similarity index 88% rename from .github/workflows/publish.yaml rename to .forgejo/workflows/publish.yaml index edbfdb2..a84f3d7 100644 --- a/.github/workflows/publish.yaml +++ b/.forgejo/workflows/publish.yaml @@ -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 @@ -26,10 +27,13 @@ jobs: run: | echo "$DONATE_PAGE_DATA" > src/components/pages/PageDonate/data.json + whoami + /opt/setup-dockerd.sh + - 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 +41,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 +54,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} deploy: - runs-on: ubuntu-latest + runs-on: node22 needs: publish steps: - uses: teidesu/desu-deploy@main diff --git a/eslint.config.js b/eslint.config.js index 2461ea5..e0a4af8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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 }], diff --git a/src/backend/service/last-seen/bsky.ts b/src/backend/service/last-seen/bsky.ts index b4b81e5..cd122bd 100644 --- a/src/backend/service/last-seen/bsky.ts +++ b/src/backend/service/last-seen/bsky.ts @@ -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>({ +export const bskyLastSeen = new AsyncResource({ async fetcher() { const res = await ffetch(ENDPOINT, { query: { @@ -29,8 +31,24 @@ export const bskyLastSeen = new AsyncResource>({ })), })) + 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, } }, diff --git a/src/backend/service/last-seen/forgejo.ts b/src/backend/service/last-seen/forgejo.ts new file mode 100644 index 0000000..b8ad3aa --- /dev/null +++ b/src/backend/service/last-seen/forgejo.ts @@ -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, 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({ + 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, +}) diff --git a/src/backend/service/last-seen/github.ts b/src/backend/service/last-seen/github.ts index ad23132..84afbd2 100644 --- a/src/backend/service/last-seen/github.ts +++ b/src/backend/service/last-seen/github.ts @@ -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>({ +export const githubLastSeen = new AsyncResource({ async fetcher() { const res = await ffetch(ENDPOINT, { headers: { @@ -25,8 +27,38 @@ export const githubLastSeen = new AsyncResource>({ }, }).parsedJson(z.array(schema)) + const data = res[0] + + const eventTextMapper: Record 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, } }, diff --git a/src/backend/service/last-seen/index.ts b/src/backend/service/last-seen/index.ts index 8510fb4..fdc74e7 100644 --- a/src/backend/service/last-seen/index.ts +++ b/src/backend/service/last-seen/index.ts @@ -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 = { - 'Просмотрено': '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> = { - 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 } diff --git a/src/backend/service/last-seen/lastfm.ts b/src/backend/service/last-seen/lastfm.ts index 04f4cc9..c253d1b 100644 --- a/src/backend/service/last-seen/lastfm.ts +++ b/src/backend/service/last-seen/lastfm.ts @@ -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 const ResponseSchema = z.object({ recenttracks: z.object({ @@ -34,7 +35,7 @@ const ResponseSchema = z.object({ }), }) -export const lastfm = new AsyncResource({ +export const lastfm = new AsyncResource({ async fetcher({ current }) { const res = await ffetch('https://ws.audioscrobbler.com/2.0/', { query: { @@ -43,7 +44,7 @@ export const lastfm = new AsyncResource({ 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({ } 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, } }, diff --git a/src/backend/service/last-seen/shikimori.ts b/src/backend/service/last-seen/shikimori.ts index 0a9ac66..d5bc308 100644 --- a/src/backend/service/last-seen/shikimori.ts +++ b/src/backend/service/last-seen/shikimori.ts @@ -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>({ +export const shikimoriLastSeen = new AsyncResource({ 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 = { + 'Просмотрено': '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, } }, diff --git a/src/layouts/DefaultLayout/DefaultLayout.astro b/src/layouts/DefaultLayout/DefaultLayout.astro index c78c855..ccf8f2c 100644 --- a/src/layouts/DefaultLayout/DefaultLayout.astro +++ b/src/layouts/DefaultLayout/DefaultLayout.astro @@ -19,7 +19,7 @@ import Header from './Header.astro' {' '} {import.meta.env.VITE_BUILD_DATE} {' / '} - + source code