diff --git a/ios/patching/001-social-app-ios-config.patch b/ios/patching/001-social-app-ios-config.patch index b132480..49a4a74 100644 --- a/ios/patching/001-social-app-ios-config.patch +++ b/ios/patching/001-social-app-ios-config.patch @@ -1,5 +1,5 @@ diff --git a/app.config.js b/app.config.js -index 246d8abd3..ed8f7b2b2 100644 +index ca417206b..0807d7cd3 100644 --- a/app.config.js +++ b/app.config.js @@ -18,10 +18,7 @@ module.exports = function (_config) { @@ -49,7 +49,7 @@ index 246d8abd3..ed8f7b2b2 100644 infoPlist: { UIBackgroundModes: ['remote-notification'], NSCameraUsageDescription: -@@ -113,7 +107,7 @@ module.exports = function (_config) { +@@ -113,7 +108,7 @@ module.exports = function (_config) { entitlements: { 'com.apple.developer.kernel.increased-memory-limit': true, 'com.apple.developer.kernel.extended-virtual-addressing': true, @@ -58,7 +58,7 @@ index 246d8abd3..ed8f7b2b2 100644 }, privacyManifests: { NSPrivacyCollectedDataTypes: [ -@@ -175,14 +169,14 @@ module.exports = function (_config) { +@@ -175,14 +170,14 @@ module.exports = function (_config) { barStyle: 'light-content', }, android: { @@ -75,7 +75,7 @@ index 246d8abd3..ed8f7b2b2 100644 intentFilters: [ { action: 'VIEW', -@@ -190,7 +184,7 @@ module.exports = function (_config) { +@@ -190,7 +185,7 @@ module.exports = function (_config) { data: [ { scheme: 'https', @@ -84,7 +84,7 @@ index 246d8abd3..ed8f7b2b2 100644 }, IS_DEV && { scheme: 'http', -@@ -213,9 +207,9 @@ module.exports = function (_config) { +@@ -213,9 +208,9 @@ module.exports = function (_config) { : undefined, codeSigningMetadata: UPDATES_ENABLED ? { @@ -97,7 +97,7 @@ index 246d8abd3..ed8f7b2b2 100644 : undefined, checkAutomatically: 'NEVER', }, -@@ -225,7 +219,7 @@ module.exports = function (_config) { +@@ -225,7 +220,7 @@ module.exports = function (_config) { 'expo-web-browser', [ 'react-native-edge-to-edge', @@ -106,7 +106,7 @@ index 246d8abd3..ed8f7b2b2 100644 ], USE_SENTRY && [ '@sentry/react-native/expo', -@@ -264,7 +258,6 @@ module.exports = function (_config) { +@@ -265,7 +260,6 @@ module.exports = function (_config) { networkInstrumentation: true, }, ], @@ -114,7 +114,7 @@ index 246d8abd3..ed8f7b2b2 100644 './plugins/withGradleJVMHeapSizeIncrease.js', './plugins/withAndroidManifestLargeHeapPlugin.js', './plugins/withAndroidManifestFCMIconPlugin.js', -@@ -272,8 +265,6 @@ module.exports = function (_config) { +@@ -273,8 +267,6 @@ module.exports = function (_config) { './plugins/withAndroidStylesAccentColorPlugin.js', './plugins/withAndroidDayNightThemePlugin.js', './plugins/withAndroidNoJitpackPlugin.js', @@ -123,16 +123,16 @@ index 246d8abd3..ed8f7b2b2 100644 [ 'expo-font', { -@@ -386,7 +377,7 @@ module.exports = function (_config) { +@@ -387,7 +379,7 @@ module.exports = function (_config) { }, }, ], - ['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}], + ['expo-screen-orientation', { initialOrientation: 'PORTRAIT_UP' }], ['expo-location'], - ].filter(Boolean), - extra: { -@@ -394,30 +385,7 @@ module.exports = function (_config) { + [ + 'expo-contacts', +@@ -402,30 +394,7 @@ module.exports = function (_config) { build: { experimental: { ios: { diff --git a/ios/patching/004-social-app-ios-core.patch b/ios/patching/004-social-app-ios-core.patch index 1f61904..c4183f3 100644 --- a/ios/patching/004-social-app-ios-core.patch +++ b/ios/patching/004-social-app-ios-core.patch @@ -1,22 +1,22 @@ diff --git a/src/App.native.tsx b/src/App.native.tsx -index fb3008627..539ebc055 100644 +index 2c4d6fa41..b69e2b18d 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx -@@ -92,7 +92,7 @@ if (isAndroid) { +@@ -95,7 +95,7 @@ if (isAndroid) { * Begin geolocation ASAP */ Geo.resolve() -prefetchAgeAssuranceConfig() +// // // prefetchAgeAssuranceConfig() + prefetchLiveEvents() function InnerApp() { - const [isReady, setIsReady] = React.useState(false) diff --git a/src/routes.ts b/src/routes.ts -index 1ed913bb2..c80340edb 100644 +index f325539c7..3e2c7b3eb 100644 --- a/src/routes.ts +++ b/src/routes.ts -@@ -71,8 +71,8 @@ export const router = new Router({ - MiscellaneousNotificationSettings: '/settings/notifications/miscellaneous', +@@ -72,9 +72,11 @@ export const router = new Router({ + FindContactsSettings: '/settings/find-contacts', // support Support: '/support', - PrivacyPolicy: '/support/privacy', @@ -24,8 +24,11 @@ index 1ed913bb2..c80340edb 100644 + PrivacyPolicy: 'https://syu.is/about/support/privacy-policy', + TermsOfService: 'https://syu.is/about/support/tos', CommunityGuidelines: '/support/community-guidelines', ++ License: 'https://syu.is/about/support/license', ++ AppInfo: 'https://syu.is/about/support/app', CopyrightPolicy: '/support/copyright', // hashtags + Hashtag: '/hashtag/:tag', diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 5c8ce3b97..ee85beb08 100644 --- a/src/state/session/agent.ts diff --git a/ios/patching/009-social-app-ios-license.patch b/ios/patching/009-social-app-ios-license.patch index ae4e3ee..b2c7373 100644 --- a/ios/patching/009-social-app-ios-license.patch +++ b/ios/patching/009-social-app-ios-license.patch @@ -1,16 +1,17 @@ diff --git a/src/Navigation.tsx b/src/Navigation.tsx -index fa33a9d56..13af087c2 100644 +index fde223bd0..0e57c27a6 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx -@@ -62,6 +62,7 @@ import {NotFoundScreen} from '#/view/screens/NotFound' +@@ -66,6 +66,8 @@ import {NotFoundScreen} from '#/view/screens/NotFound' import {NotificationsScreen} from '#/view/screens/Notifications' import {PostThreadScreen} from '#/view/screens/PostThread' import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy' +import {LicenseScreen} from '#/view/screens/License' ++import {AppInfoScreen} from '#/view/screens/AppInfo' import {ProfileScreen} from '#/view/screens/Profile' import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy' - import {Storybook} from '#/view/screens/Storybook' -@@ -335,6 +336,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { + import {StorybookScreen} from '#/view/screens/Storybook' +@@ -337,6 +339,16 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { getComponent={() => TermsOfServiceScreen} options={{title: title(msg`Terms of Service`)}} /> @@ -18,157 +19,25 @@ index fa33a9d56..13af087c2 100644 + name="License" + getComponent={() => LicenseScreen} + options={{title: title(msg`License`)}} ++ /> ++ AppInfoScreen} ++ options={{title: title(msg`App Info`)}} + /> CommunityGuidelinesScreen} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts -index c315a8341..9b2f50a83 100644 +index 0a3a0d545..ec6c43ff5 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts -@@ -39,6 +39,7 @@ export type CommonNavigatorParams = { +@@ -39,6 +39,8 @@ export type CommonNavigatorParams = { Support: undefined PrivacyPolicy: undefined TermsOfService: undefined + License: undefined ++ AppInfo: undefined CommunityGuidelines: undefined CopyrightPolicy: undefined LanguageSettings: undefined -diff --git a/src/view/screens/License.tsx b/src/view/screens/License.tsx -new file mode 100644 -index 000000000..87f52a972 ---- /dev/null -+++ b/src/view/screens/License.tsx -@@ -0,0 +1,132 @@ -+import React from 'react' -+import { ScrollView, Text as RNText, StyleSheet } from 'react-native' -+import * as Layout from '#/components/Layout' -+import {useSetTitle} from '#/lib/hooks/useSetTitle' -+import {atoms as a, useTheme} from '#/alf' -+ -+export function LicenseScreen() { -+ useSetTitle('License') -+ const t = useTheme() -+ -+ return ( -+ -+ -+ -+ -+ License -+ -+ -+ -+ -+ -+ -+ This application is based on Bluesky Social App. -+ -+ -+ -+ https://github.com/bluesky-social/social-app -+ -+ -+ MIT License -+ -+ -+ Copyright (c) 2022-2025 Bluesky PBC -+ -+ -+ -+ Permission is hereby granted, free of charge, to any person obtaining a copy -+ of this software and associated documentation files (the "Software"), to deal -+ in the Software without restriction, including without limitation the rights -+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -+ copies of the Software, and to permit persons to whom the Software is -+ furnished to do so, subject to the following conditions: -+ -+ -+ -+ The above copyright notice and this permission notice shall be included in all -+ copies or substantial portions of the Software. -+ -+ -+ -+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -+ SOFTWARE. -+ -+ -+ 日本語訳(参考) -+ -+ -+ 本ソフトウェアおよび関連文書ファイル(以下「ソフトウェア」)のコピーを取得する -+ すべての人に対し、ソフトウェアを無制限に扱うことを無償で許可します。これには、 -+ ソフトウェアのコピーを使用、複製、変更、結合、公開、配布、サブライセンス、 -+ および/または販売する権利、ならびにソフトウェアを提供する相手にそうした行為を -+ 許可する権利が含まれますが、これらに限定されません。 -+ -+ -+ -+ 上記の著作権表示および本許諾表示を、ソフトウェアのすべてのコピーまたは -+ 重要な部分に記載するものとします。 -+ -+ -+ -+ ソフトウェアは「現状のまま」で提供され、明示黙示を問わず、商品性、特定目的への -+ 適合性、および権利非侵害についての保証を含む、いかなる種類の保証もなされません。 -+ いかなる場合においても、作者または著作権者は、契約行為、不法行為、またはそれ以外で -+ あろうと、ソフトウェアに起因または関連し、あるいはソフトウェアの使用または -+ その他の扱いによって生じる一切の請求、損害、その他の義務について責任を負わないものとします。 -+ -+ -+ -+ Original License: https://github.com/bluesky-social/social-app/blob/main/LICENSE -+ -+ -+ -+ -+ ) -+} -+ -+const styles = StyleSheet.create({ -+ title: { -+ fontSize: 24, -+ fontWeight: 'bold', -+ marginBottom: 16, -+ }, -+ text: { -+ fontSize: 14, -+ marginBottom: 12, -+ lineHeight: 20, -+ }, -+ link: { -+ fontSize: 14, -+ marginBottom: 12, -+ color: '#0066cc', -+ }, -+ sectionTitle: { -+ fontSize: 18, -+ fontWeight: 'bold', -+ marginTop: 16, -+ marginBottom: 12, -+ }, -+ sectionTitle2: { -+ fontSize: 18, -+ fontWeight: 'bold', -+ marginTop: 24, -+ marginBottom: 12, -+ }, -+ mono: { -+ fontSize: 14, -+ marginBottom: 12, -+ fontFamily: 'monospace', -+ }, -+ footer: { -+ fontSize: 12, -+ marginTop: 24, -+ color: '#666666', -+ }, -+}) diff --git a/ios/patching/011-social-app-ios-splash-license-footer.patch b/ios/patching/011-social-app-ios-splash-license-footer.patch index a127fa0..8fc36bb 100644 --- a/ios/patching/011-social-app-ios-splash-license-footer.patch +++ b/ios/patching/011-social-app-ios-splash-license-footer.patch @@ -1,15 +1,5 @@ -diff --git a/src/routes.ts b/src/routes.ts ---- a/src/routes.ts -+++ b/src/routes.ts -@@ -74,6 +74,7 @@ export const router = new Router({ - PrivacyPolicy: 'https://syu.is/about/support/privacy-policy', - TermsOfService: 'https://syu.is/about/support/tos', - CommunityGuidelines: '/support/community-guidelines', -+ License: 'https://syu.is/about/support/license', - CopyrightPolicy: '/support/copyright', - // hashtags - Hashtag: '/hashtag/:tag', diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx +index 3442d1bdf..8ed9e3d0d 100644 --- a/src/view/com/auth/SplashScreen.tsx +++ b/src/view/com/auth/SplashScreen.tsx @@ -1,4 +1,5 @@ @@ -33,7 +23,7 @@ diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen - What's up? - - + @@ -58,12 +48,13 @@ diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx +index 22dd23d7f..7ceb3800e 100644 --- a/src/view/com/auth/SplashScreen.web.tsx +++ b/src/view/com/auth/SplashScreen.web.tsx @@ -94,14 +94,6 @@ export const SplashScreen = ({ )} - + - What's up? - - + LicenseScreen} - options={{title: title(msg`License`)}} - /> -+ AppInfoScreen} -+ options={{title: title(msg`App Info`)}} -+ /> - CommunityGuidelinesScreen} -diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts ---- a/src/lib/routes/types.ts -+++ b/src/lib/routes/types.ts -@@ -40,6 +40,7 @@ export type CommonNavigatorParams = { - PrivacyPolicy: undefined - TermsOfService: undefined - License: undefined -+ AppInfo: undefined - CommunityGuidelines: undefined - CopyrightPolicy: undefined - LanguageSettings: undefined -diff --git a/src/routes.ts b/src/routes.ts ---- a/src/routes.ts -+++ b/src/routes.ts -@@ -75,6 +75,7 @@ export const router = new Router({ - TermsOfService: 'https://syu.is/about/support/tos', - CommunityGuidelines: '/support/community-guidelines', - License: 'https://syu.is/about/support/license', -+ AppInfo: 'https://syu.is/about/support/app', - CopyrightPolicy: '/support/copyright', - // hashtags - Hashtag: '/hashtag/:tag', -diff --git a/src/view/screens/AppInfo.tsx b/src/view/screens/AppInfo.tsx -new file mode 100644 -index 000000000..000000001 ---- /dev/null -+++ b/src/view/screens/AppInfo.tsx -@@ -0,0 +1,310 @@ -+import React, {useState} from 'react' -+import { -+ View, -+ ScrollView, -+ StyleSheet, -+ Pressable, -+ Image, -+} from 'react-native' -+import * as Clipboard from 'expo-clipboard' -+import * as WebBrowser from 'expo-web-browser' -+import {Trans} from '@lingui/macro' -+ -+import * as Layout from '#/components/Layout' -+import {Text} from '#/components/Typography' -+import {useSetTitle} from '#/lib/hooks/useSetTitle' -+import {atoms as a, useTheme} from '#/alf' -+ -+const APP_VERSION = '1.111.0' -+const APP_NAME = 'Aiat' -+const BITCOIN_ADDRESS = '3BqHXxraZyBapyNpJmniJDh9zqzuB8aoRr' -+ -+export function AppInfoScreen() { -+ useSetTitle('App Info') -+ const t = useTheme() -+ const [copied, setCopied] = useState(false) -+ -+ const copyToClipboard = async (text: string) => { -+ try { -+ await Clipboard.setStringAsync(text) -+ setCopied(true) -+ setTimeout(() => setCopied(false), 2000) -+ } catch (e) { -+ console.log('Clipboard not available') -+ } -+ } -+ -+ const openUrl = (url: string) => { -+ WebBrowser.openBrowserAsync(url) -+ } -+ -+ return ( -+ -+ -+ -+ -+ -+ App Info -+ -+ -+ -+ -+ -+ -+ {/* App Header */} -+ -+ -+ -+ -+ -+ {APP_NAME} -+ -+ -+ v{APP_VERSION} -+ -+ -+ -+ {/* Description Section */} -+ -+ -+ {APP_NAME} is a social networking application based on AT Protocol. -+ Connect with your community on syu.is. -+ -+ -+ -+ {/* App Information Section */} -+ -+ -+ App Information -+ -+ -+ -+ -+ Version -+ -+ -+ {APP_VERSION} -+ -+ -+ -+ -+ Category -+ -+ -+ Social -+ -+ -+ -+ -+ Supported OS -+ -+ -+ iOS 26.0+ -+ -+ -+ -+ -+ Price -+ -+ -+ Free -+ -+ -+ -+ -+ -+ {/* Developer Section */} -+ -+ -+ Developer -+ -+ -+ syui -+ -+ -+ openUrl('https://github.com/syui')} -+ style={[styles.linkRow, t.atoms.border_contrast_low]}> -+ -+ GitHub -+ -+ -+ github.com/syui -+ -+ -+ -+ -+ openUrl('https://syu.is/syui')} -+ style={[styles.linkRow, t.atoms.border_contrast_low]}> -+ -+ ATProto -+ -+ -+ syu.is/syui -+ -+ -+ -+ -+ -+ {/* Bitcoin Section */} -+ -+ -+ Bitcoin -+ -+ copyToClipboard(BITCOIN_ADDRESS)} -+ style={styles.bitcoinRow}> -+ -+ -+ {BITCOIN_ADDRESS} -+ -+ -+ {copied ? 'copied!' : 'copy'} -+ -+ -+ -+ -+ {/* Copyright */} -+ -+ -+ © syui -+ -+ -+ -+ -+ -+ ) -+} -+ -+const styles = StyleSheet.create({ -+ appHeader: { -+ alignItems: 'center', -+ marginBottom: 24, -+ }, -+ appIconContainer: { -+ width: 80, -+ height: 80, -+ borderRadius: 18, -+ overflow: 'hidden', -+ marginBottom: 12, -+ }, -+ appIcon: { -+ width: '100%', -+ height: '100%', -+ }, -+ appName: { -+ fontSize: 24, -+ fontWeight: 'bold', -+ marginBottom: 4, -+ }, -+ appVersion: { -+ fontSize: 14, -+ }, -+ section: { -+ marginBottom: 16, -+ borderRadius: 16, -+ padding: 16, -+ }, -+ sectionTitle: { -+ fontSize: 13, -+ fontWeight: '600', -+ textTransform: 'uppercase', -+ letterSpacing: 0.5, -+ marginBottom: 12, -+ }, -+ description: { -+ fontSize: 15, -+ lineHeight: 22, -+ }, -+ infoGrid: { -+ flexDirection: 'row', -+ flexWrap: 'wrap', -+ gap: 8, -+ }, -+ infoItem: { -+ flex: 1, -+ minWidth: '45%', -+ alignItems: 'center', -+ borderRadius: 12, -+ padding: 12, -+ }, -+ infoLabel: { -+ fontSize: 11, -+ textTransform: 'uppercase', -+ letterSpacing: 0.5, -+ marginBottom: 4, -+ }, -+ infoValue: { -+ fontSize: 16, -+ fontWeight: '600', -+ textAlign: 'center', -+ }, -+ developerCard: { -+ marginBottom: 12, -+ }, -+ developerName: { -+ fontSize: 18, -+ fontWeight: '600', -+ }, -+ linkRow: { -+ flexDirection: 'row', -+ alignItems: 'center', -+ paddingVertical: 12, -+ borderTopWidth: 1, -+ }, -+ linkIcon: { -+ fontSize: 14, -+ fontWeight: '600', -+ width: 70, -+ }, -+ linkValue: { -+ flex: 1, -+ fontSize: 14, -+ }, -+ linkArrow: { -+ fontSize: 16, -+ }, -+ bitcoinRow: { -+ flexDirection: 'row', -+ alignItems: 'center', -+ backgroundColor: 'rgba(247, 147, 26, 0.08)', -+ borderRadius: 12, -+ padding: 14, -+ gap: 10, -+ }, -+ bitcoinLabel: { -+ fontSize: 18, -+ fontWeight: '600', -+ color: '#f7931a', -+ }, -+ bitcoinAddress: { -+ flex: 1, -+ fontSize: 11, -+ fontFamily: 'monospace', -+ }, -+ copyHint: { -+ fontSize: 12, -+ color: '#999999', -+ minWidth: 50, -+ textAlign: 'right', -+ }, -+ copiedHint: { -+ color: '#4CAF50', -+ fontWeight: '600', -+ }, -+ copyright: { -+ alignItems: 'center', -+ marginTop: 20, -+ marginBottom: 20, -+ }, -+ copyrightText: { -+ fontSize: 12, -+ }, -+}) diff --git a/ios/setup.zsh b/ios/setup.zsh index cf3b058..e9b4b64 100755 --- a/ios/setup.zsh +++ b/ios/setup.zsh @@ -41,7 +41,6 @@ PATCH_FILES_IOS=( "027-social-app-ios-remove-birthdate.patch" "028-social-app-ios-remove-discover-feeds.patch" "029-social-app-ios-remove-feeds-discover.patch" - "030-social-app-ios-appinfo.patch" "032-social-app-ios-feed-loggedout.patch" "033-social-app-ios-hide-profile-tabs.patch" "036-social-app-ios-homeheader-loggedout.patch" diff --git a/patching/200-feed-generator-custom.patch b/patching/200-feed-generator-custom.patch index 3ee2a6a..4d4d6ea 100644 --- a/patching/200-feed-generator-custom.patch +++ b/patching/200-feed-generator-custom.patch @@ -1,27 +1,3 @@ -diff --git a/.dockerignore b/.dockerignore -new file mode 100644 -index 0000000..3c3629e ---- /dev/null -+++ b/.dockerignore -@@ -0,0 +1 @@ -+node_modules -diff --git a/Dockerfile b/Dockerfile -new file mode 100644 -index 0000000..993c83d ---- /dev/null -+++ b/Dockerfile -@@ -0,0 +1,11 @@ -+FROM node:18-alpine -+ -+WORKDIR /app -+ -+COPY package.json yarn.lock ./ -+RUN yarn install -+ -+COPY . . -+ -+EXPOSE 3000 -+CMD ["yarn", "start"] diff --git a/package.json b/package.json index 1431a9e..6a7c33c 100644 --- a/package.json @@ -39,117 +15,6 @@ index 1431a9e..6a7c33c 100644 "@types/better-sqlite3": "^7.6.11", "@types/express": "^4.17.17", "@types/node": "^20.1.2", -diff --git a/scripts/publish.ts b/scripts/publish.ts -new file mode 100644 -index 0000000..044f1d9 ---- /dev/null -+++ b/scripts/publish.ts -@@ -0,0 +1,64 @@ -+import dotenv from 'dotenv' -+import { AtpAgent, BlobRef } from '@atproto/api' -+import fs from 'fs/promises' -+import { ids } from '../src/lexicon/lexicons' -+ -+const run = async () => { -+ dotenv.config() -+ -+ const handle = process.env.FEEDGEN_HANDLE -+ const password = process.env.FEEDGEN_PASSWORD -+ const recordName = process.env.FEEDGEN_RECORD_NAME || 'app' -+ const displayName = process.env.FEEDGEN_DISPLAY_NAME || 'App Feed' -+ const description = process.env.FEEDGEN_DESCRIPTION || 'Automated App Feed' -+ const avatar = process.env.FEEDGEN_AVATAR -+ const service = process.env.FEEDGEN_SERVICE_URL || 'https://syu.is' -+ -+ if (!handle || !password) { -+ throw new Error('Please provide FEEDGEN_HANDLE and FEEDGEN_PASSWORD environment variables') -+ } -+ -+ if (!process.env.FEEDGEN_SERVICE_DID && !process.env.FEEDGEN_HOSTNAME) { -+ throw new Error('Please provide a hostname in the .env file') -+ } -+ -+ const feedGenDid = -+ process.env.FEEDGEN_SERVICE_DID ?? `did:web:${process.env.FEEDGEN_HOSTNAME}` -+ -+ const agent = new AtpAgent({ service }) -+ await agent.login({ identifier: handle, password }) -+ -+ let avatarRef: BlobRef | undefined -+ if (avatar) { -+ let encoding: string -+ if (avatar.endsWith('png')) { -+ encoding = 'image/png' -+ } else if (avatar.endsWith('jpg') || avatar.endsWith('jpeg')) { -+ encoding = 'image/jpeg' -+ } else { -+ throw new Error('expected png or jpeg') -+ } -+ const img = await fs.readFile(avatar) -+ const blobRes = await agent.api.com.atproto.repo.uploadBlob(img, { -+ encoding, -+ }) -+ avatarRef = blobRes.data.blob -+ } -+ -+ await agent.api.com.atproto.repo.putRecord({ -+ repo: agent.session?.did ?? '', -+ collection: ids.AppBskyFeedGenerator, -+ rkey: recordName, -+ record: { -+ did: feedGenDid, -+ displayName: displayName, -+ description: description, -+ avatar: avatarRef, -+ createdAt: new Date().toISOString(), -+ }, -+ }) -+ -+ console.log('All done') -+} -+ -+run() -diff --git a/src/algos/app.ts b/src/algos/app.ts -new file mode 100644 -index 0000000..2376be9 ---- /dev/null -+++ b/src/algos/app.ts -@@ -0,0 +1,35 @@ -+import { QueryParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -+import { AppContext } from '../config' -+ -+// max 15 chars -+export const shortname = 'app' -+ -+export const handler = async (ctx: AppContext, params: QueryParams) => { -+ let builder = ctx.db -+ .selectFrom('post') -+ .selectAll() -+ .orderBy('indexedAt', 'desc') -+ .orderBy('cid', 'desc') -+ .limit(params.limit) -+ -+ if (params.cursor) { -+ const timeStr = new Date(parseInt(params.cursor, 10)).toISOString() -+ builder = builder.where('post.indexedAt', '<', timeStr) -+ } -+ const res = await builder.execute() -+ -+ const feed = res.map((row) => ({ -+ post: row.uri, -+ })) -+ -+ let cursor: string | undefined -+ const last = res.at(-1) -+ if (last) { -+ cursor = new Date(last.indexedAt).getTime().toString(10) -+ } -+ -+ return { -+ cursor, -+ feed, -+ } -+} diff --git a/src/algos/index.ts b/src/algos/index.ts index b7ee48a..102cb93 100644 --- a/src/algos/index.ts