chore: migrated to forgejo
All checks were successful
Publish and deploy / deploy (push) Successful in 6s

This commit is contained in:
alina 🌸 2025-01-04 02:50:49 +03:00
parent 082d230cd8
commit 1473d61e9a
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
10 changed files with 264 additions and 151 deletions

View file

@ -0,0 +1,62 @@
name: Publish and deploy
on:
push:
branches: [ main ]
workflow_dispatch:
concurrency:
group: deploy
cancel-in-progress: true
jobs:
# publish:
# if: github.repository == 'teidesu/tei.su' # do not run on forks
# runs-on: docker-dind
# permissions:
# contents: write
# packages: write
# steps:
# - name: Checkout
# uses: actions/checkout@v4
# - name: Prepare
# env:
# DONATE_PAGE_DATA: ${{ vars.DONATE_PAGE_DATA }}
# run: |
# echo "$DONATE_PAGE_DATA" > src/components/pages/PageDonate/data.json
# /opt/start-dockerd.sh
# - name: Login to GitHub Container Registry
# uses: docker/login-action@v3
# with:
# registry: git.stupid.fish
# username: ${{ github.actor }}
# password: ${{ secrets.PACKAGES_PAT }}
# - name: Docker meta
# id: meta
# uses: docker/metadata-action@v5
# with:
# images: git.stupid.fish/teidesu/tei.su
# tags: type=sha
# flavor: latest=true
# - name: Build and push
# uses: docker/build-push-action@v5
# with:
# context: .
# push: true
# platforms: linux/amd64
# tags: ${{ steps.meta.outputs.tags }}
# labels: ${{ steps.meta.outputs.labels }}
deploy:
runs-on: node22
# needs: publish
steps:
- uses: https://github.com/teidesu/desu-deploy@main
with:
key: ${{ secrets.DESU_DEPLOY_KEY }}
server: ${{ secrets.KOI_IP }}
service: teisu

View file

@ -1,61 +0,0 @@
name: Publish and deploy
on:
push:
branches: [ main ]
workflow_dispatch:
concurrency:
group: deploy
cancel-in-progress: true
jobs:
publish:
if: github.repository == 'teidesu/tei.su' # do not run on forks
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Prepare
env:
DONATE_PAGE_DATA: ${{ vars.DONATE_PAGE_DATA }}
run: |
echo "$DONATE_PAGE_DATA" > src/components/pages/PageDonate/data.json
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/teidesu/tei.su
tags: type=sha
flavor: latest=true
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
deploy:
runs-on: ubuntu-latest
needs: publish
steps:
- uses: teidesu/desu-deploy@main
with:
key: ${{ secrets.DEPLOY_KEY }}
server: ${{ secrets.DEPLOY_SERVER }}
wireguard: ${{ secrets.DEPLOY_WG }}
service: teisu

View file

@ -7,6 +7,7 @@ export default antfu({
typescript: true, typescript: true,
astro: true, astro: true,
solid: true, solid: true,
yaml: false,
rules: { rules: {
'curly': ['error', 'multi-line'], 'curly': ['error', 'multi-line'],
'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],

View file

@ -3,6 +3,8 @@ import { z } from 'zod'
import { ffetch } from '../../utils/fetch.ts' 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 ENDPOINT = 'https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed'
const TTL = 3 * 60 * 60 * 1000 // 3 hours const TTL = 3 * 60 * 60 * 1000 // 3 hours
const STALE_TTL = 8 * 60 * 60 * 1000 // 8 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() { async fetcher() {
const res = await ffetch(ENDPOINT, { const res = await ffetch(ENDPOINT, {
query: { 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 { return {
data: res.feed[0].post, 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: null,
expiresIn: TTL, expiresIn: TTL,
} }
}, },

View 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,
})

View file

@ -3,6 +3,8 @@ import { z } from 'zod'
import { ffetch } from '../../utils/fetch.ts' 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 ENDPOINT = 'https://api.github.com/users/teidesu/events/public?per_page=1'
const TTL = 1 * 60 * 60 * 1000 // 1 hour const TTL = 1 * 60 * 60 * 1000 // 1 hour
const STALE_TTL = 4 * 60 * 60 * 1000 // 4 hours const STALE_TTL = 4 * 60 * 60 * 1000 // 4 hours
@ -16,7 +18,7 @@ const schema = z.object({
created_at: z.string(), created_at: z.string(),
}) })
export const githubLastSeen = new AsyncResource<z.infer<typeof schema>>({ export const githubLastSeen = new AsyncResource<LastSeenItem | null>({
async fetcher() { async fetcher() {
const res = await ffetch(ENDPOINT, { const res = await ffetch(ENDPOINT, {
headers: { headers: {
@ -25,8 +27,38 @@ export const githubLastSeen = new AsyncResource<z.infer<typeof schema>>({
}, },
}).parsedJson(z.array(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 { return {
data: res[0], 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: null,
expiresIn: TTL, expiresIn: TTL,
} }
}, },

View file

@ -4,6 +4,7 @@ import { bskyLastSeen } from './bsky.ts'
import { githubLastSeen } from './github' import { githubLastSeen } from './github'
import { lastfm } from './lastfm' import { lastfm } from './lastfm'
import { shikimoriLastSeen } from './shikimori' import { shikimoriLastSeen } from './shikimori'
import { forgejoLastSeen } from './forgejo.ts'
export interface LastSeenItem { export interface LastSeenItem {
source: string source: string
@ -20,93 +21,33 @@ export async function fetchLastSeen() {
bskyData, bskyData,
shikimoriData, shikimoriData,
githubData, githubData,
forgejoData,
] = await Promise.all([ ] = await Promise.all([
lastfm.get(), lastfm.get(),
bskyLastSeen.get(), bskyLastSeen.get(),
shikimoriLastSeen.get(), shikimoriLastSeen.get(),
githubLastSeen.get(), githubLastSeen.get(),
forgejoLastSeen.get(),
]) ])
const res: LastSeenItem[] = [] const res: LastSeenItem[] = []
if (lastfmData) { if (lastfmData) res.push(lastfmData)
res.push({ if (bskyData) res.push(bskyData)
source: 'last.fm', if (shikimoriData) res.push(shikimoriData)
sourceLink: 'https://last.fm/user/teidesu',
time: Number(lastfmData.date!.uts) * 1000, if (githubData && forgejoData) {
text: `${lastfmData.name} ${lastfmData.artist['#text']}`, // only push the last one
link: lastfmData.url, 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) { return res
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)
} }

View file

@ -5,6 +5,8 @@ import { zodValidate } from '~/utils/zod'
import { env } from '~/backend/env' import { env } from '~/backend/env'
import { ffetch } from '../../utils/fetch.ts' import { ffetch } from '../../utils/fetch.ts'
import type { LastSeenItem } from './index.ts'
const LASTFM_TTL = 1000 * 60 * 5 // 5 minutes const LASTFM_TTL = 1000 * 60 * 5 // 5 minutes
const LASTFM_STALE_TTL = 1000 * 60 * 60 // 1 hour const LASTFM_STALE_TTL = 1000 * 60 * 60 // 1 hour
const LASTFM_USERNAME = 'teidesu' const LASTFM_USERNAME = 'teidesu'
@ -19,7 +21,6 @@ const LastfmTrack = z.object({
nowplaying: z.literal('true'), nowplaying: z.literal('true'),
}).partial().optional(), }).partial().optional(),
}) })
export type LastfmTrack = z.infer<typeof LastfmTrack>
const ResponseSchema = z.object({ const ResponseSchema = z.object({
recenttracks: 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 }) { async fetcher({ current }) {
const res = await ffetch('https://ws.audioscrobbler.com/2.0/', { const res = await ffetch('https://ws.audioscrobbler.com/2.0/', {
query: { query: {
@ -43,7 +44,7 @@ export const lastfm = new AsyncResource<LastfmTrack>({
api_key: LASTFM_TOKEN, api_key: LASTFM_TOKEN,
format: 'json', format: 'json',
limit: '1', limit: '1',
from: current?.date?.uts, from: current ? Math.floor(current.time / 1000) : undefined,
}, },
}).parsedJson(ResponseSchema) }).parsedJson(ResponseSchema)
@ -55,7 +56,13 @@ export const lastfm = new AsyncResource<LastfmTrack>({
} }
return { 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, expiresIn: LASTFM_TTL,
} }
}, },

View file

@ -3,6 +3,8 @@ import { z } from 'zod'
import { ffetch } from '../../utils/fetch.ts' import { ffetch } from '../../utils/fetch.ts'
import type { LastSeenItem } from './index.ts'
const ENDPOINT = 'https://shikimori.one/api/users/698215/history?limit=1' const ENDPOINT = 'https://shikimori.one/api/users/698215/history?limit=1'
const TTL = 3 * 60 * 60 * 1000 // 3 hours const TTL = 3 * 60 * 60 * 1000 // 3 hours
const STALE_TTL = 8 * 60 * 60 * 1000 // 8 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() { 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 { return {
data: res[0], data: null,
expiresIn: TTL, expiresIn: TTL,
} }
}, },

View file

@ -19,7 +19,7 @@ import Header from './Header.astro'
{' '} {' '}
{import.meta.env.VITE_BUILD_DATE} {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 source code
</Link> </Link>
</div> </div>