unoffical wafrn mirror wafrn.net
atproto social-network activitypub

WIP

+163 -31
+1 -1
packages/backend/index.ts
··· 47 47 import { Worker } from 'bullmq' 48 48 import expressWs from 'express-ws' 49 49 import websocketRoutes from './routes/websocket.js' 50 - import followHashtagRoutes from './routes/followHashtags.js' 50 + import { followHashtagRoutes } from './routes/followHashtags.js' 51 51 import { completeEnvironment } from './utils/backendOptions.js' 52 52 import cron from 'node-cron' 53 53 import { nukeBannedUsers } from './utils/maintenanceTasks/nukeBannedUsers.js'
+16
packages/backend/migrations/2025.09.22T18.44.36.addBskyInviteCodeToUserForMigration.ts
··· 1 + import { DataTypes, Sequelize } from 'sequelize' 2 + import { Migration } from '../migrate.js' 3 + import { FederatedHost } from '../models/federatedHost.js' 4 + 5 + export const up: Migration = async (params) => { 6 + const queryInterface = params.context 7 + await queryInterface.addColumn('users', 'bskyInviteCode', { 8 + type: DataTypes.STRING, 9 + defaultValue: '', 10 + allowNull: true 11 + }) 12 + } 13 + export const down: Migration = async (params) => { 14 + const queryInterface = params.context 15 + await queryInterface.removeColumn('users', 'bskyInviteCode') 16 + }
+16 -11
packages/backend/models/user.ts
··· 80 80 emailVerified: Boolean | null 81 81 selfDeleted: Boolean | null 82 82 userMigratedTo: String | null 83 + bskyInviteCode: String | null 83 84 } 84 85 85 86 @Table({ ··· 101 102 'registerIp', 102 103 'bskyAuthData', 103 104 'bskyAppPassword', 104 - 'birthDate' 105 + 'birthDate', 106 + 'bskyInviteCode' 105 107 ] 106 108 } 107 109 } ··· 292 294 type: DataType.STRING 293 295 }) 294 296 declare followingCollectionUrl: string 297 + 298 + @Column({ 299 + allowNull: true, 300 + type: DataType.STRING 301 + }) 302 + declare bskyInviteCode: string 295 303 296 304 @Column({ 297 305 allowNull: true, ··· 562 570 563 571 // the username part of the handle, without the domain for both bsky and fedi 564 572 get shortHandle() { 565 - if (this.isBlueskyUser) 566 - return this.url.split('@')[1].split('.')[0]; 573 + if (this.isBlueskyUser) return this.url.split('@')[1].split('.')[0] 567 574 568 - if (this.isFediverseUser) 569 - return this.url.split('@')[1] 575 + if (this.isFediverseUser) return this.url.split('@')[1] 570 576 571 577 return this.url 572 578 } 573 579 574 580 // the username part of the handle. For bluesky also includes the domain, but for fedi it doesn't 575 581 get longHandle() { 576 - if (this.isBlueskyUser || this.isFediverseUser) 577 - return this.url.split('@')[1] 582 + if (this.isBlueskyUser || this.isFediverseUser) return this.url.split('@')[1] 578 583 579 584 return this.url 580 585 } ··· 613 618 username: string 614 619 handle: string 615 620 domain: string 616 - type: "fediverse" | "bluesky" | "local" 621 + type: 'fediverse' | 'bluesky' | 'local' 617 622 } 618 623 619 624 export function splitHandle(handleString: string): HandleData { ··· 627 632 handle: handleString, 628 633 username: username, 629 634 domain: domain, 630 - type: "fediverse" 635 + type: 'fediverse' 631 636 } 632 637 } else if (userData.length === 2 && userData[0] == '') { 633 638 const handle = userData[1] ··· 638 643 handle: handle, 639 644 username: username, 640 645 domain: domain, 641 - type: "bluesky" 646 + type: 'bluesky' 642 647 } 643 648 } 644 649 } ··· 646 651 username: handleString, 647 652 handle: handleString, 648 653 domain: completeEnvironment.instanceUrl, 649 - type: "local" 654 + type: 'local' 650 655 } 651 656 } 652 657
+4 -16
packages/backend/routes/followHashtags.ts
··· 4 4 import { UserFollowHashtags } from '../models/userFollowHashtag.js' 5 5 import { Queue } from 'bullmq' 6 6 import { completeEnvironment } from '../utils/backendOptions.js' 7 + import { forceUpdateCacheDidsAtThread } from '../atproto/cache/getCacheAtDids.js' 7 8 8 - export default function followHashtagRoutes(app: Application) { 9 + function followHashtagRoutes(app: Application) { 9 10 app.post('/api/followHashtag', authenticateToken, async (req: AuthorizedRequest, res: Response) => { 10 11 let success = false 11 12 if (req.body.hashtag && typeof req.body.hashtag === 'string') { ··· 54 55 }) 55 56 ) 56 57 }) 58 + } 57 59 58 - async function forceUpdateCacheDidsAtThread() { 59 - const forceUpdaDidsteQueue = new Queue('forceUpdateDids', { 60 - connection: completeEnvironment.bullmqConnection, 61 - defaultJobOptions: { 62 - removeOnComplete: true, 63 - attempts: 3, 64 - backoff: { 65 - type: 'exponential', 66 - delay: 1000 67 - } 68 - } 69 - }) 70 - await forceUpdaDidsteQueue.add('forceUpdateDids', {}) 71 - } 72 - } 60 + export { followHashtagRoutes }
+126 -3
packages/backend/routes/users.ts
··· 62 62 import { completeEnvironment } from '../utils/backendOptions.js' 63 63 import { sendUpdateProfile } from '../utils/activitypub/sendUpdateProfile.js' 64 64 import axios from 'axios' 65 + import { getAtprotoUser } from '../atproto/utils/getAtprotoUser.js' 66 + import { getAllLocalUserIds } from '../utils/cacheGetters/getAllLocalUserIds.js' 67 + import { syncBskyFollowersAndFollowing } from '../utils/atproto/syncBskyFollowersAndFollowing.js' 65 68 66 69 const markdownConverter = new showdown.Converter({ 67 70 simplifiedAutoLink: true, ··· 162 165 } 163 166 164 167 const userWithEmail = User.create(user) 165 - 168 + 166 169 const instanceUrl = completeEnvironment.instanceUrl.startsWith('http') 167 170 ? completeEnvironment.instanceUrl 168 171 : `https://${completeEnvironment.instanceUrl}` ··· 172 175 } catch (err) { 173 176 console.error('cannot use `completeEnvironment.instanceUrl` in `new URL` constructor') 174 177 } 175 - 178 + 176 179 const email = req.body.email.toLowerCase() 177 180 const activationLink = `${instanceUrl}/activate/${encodeURIComponent(email)}/${activationCode}` 178 181 const mailHeader = `Welcome to ${instanceHost}, please verify your email!` ··· 1140 1143 } 1141 1144 }) 1142 1145 1146 + app.get('/api/get-bsky-invite-code', authenticateToken, async (req: AuthorizedRequest, res: Response) => { 1147 + if (!completeEnvironment.enableBsky) { 1148 + return res.status(500).send({ 1149 + error: true, 1150 + message: `This instance does not have bluesky enabled at this moment` 1151 + }) 1152 + } 1153 + 1154 + const userId = req.jwtData?.userId as string 1155 + 1156 + let user: User | null = null 1157 + try { 1158 + user = await User.scope('full').findByPk(userId) 1159 + } catch (error) { 1160 + logger.error({ 1161 + message: `Error finding current user`, 1162 + error: error 1163 + }) 1164 + return res.status(500).send({ 1165 + error: true, 1166 + message: `Error finding current user` 1167 + }) 1168 + } 1169 + 1170 + if (!user) { 1171 + return res.status(404).send({ 1172 + error: true, 1173 + message: `Current user not found in database` 1174 + }) 1175 + } 1176 + 1177 + if (user.bskyInviteCode) { 1178 + return res.send({ code: user.bskyInviteCode }) 1179 + } else { 1180 + const authString = Buffer.from('admin:' + completeEnvironment.bskyPdsAdminPassword).toString('base64') 1181 + if (user.bskyDid) { 1182 + const deleteAccountReply = await axios.post( 1183 + 'https://' + completeEnvironment.bskyPdsUrl + '/xrpc/com.atproto.admin.deleteAccount', 1184 + { did: user.bskyDid }, 1185 + { 1186 + headers: { 1187 + 'Content-Type': 'application/json', 1188 + Authorization: 'Basic ' + authString 1189 + } 1190 + } 1191 + ) 1192 + user.bskyDid = '' 1193 + user.enableBsky = false 1194 + await user.save() 1195 + } 1196 + const inviteCodesReply: { code: string } = await axios.post( 1197 + 'https://' + completeEnvironment.bskyPdsUrl + '/xrpc/com.atproto.server.createInviteCode', 1198 + { count: 1 }, 1199 + { 1200 + headers: { 1201 + 'Content-Type': 'application/json', 1202 + Authorization: 'Basic ' + authString 1203 + } 1204 + } 1205 + ) 1206 + user.bskyInviteCode = inviteCodesReply.code 1207 + await user.save() 1208 + return res.send({ code: inviteCodesReply.code }) 1209 + } 1210 + }) 1211 + 1212 + app.post('/api/connect-bsky-account', authenticateToken, async (req: AuthorizedRequest, res: Response) => { 1213 + if (!completeEnvironment.enableBsky) { 1214 + return res.status(500).send({ 1215 + error: true, 1216 + message: `This instance does not have bluesky enabled at this moment` 1217 + }) 1218 + } 1219 + 1220 + const userId = req.jwtData?.userId as string 1221 + const user = await User.scope('full').findByPk(userId) 1222 + const bskyUrl = req.body.url 1223 + const pasword = req.body.password 1224 + if (user && bskyUrl && pasword) { 1225 + const localIds = await getAllLocalUserIds() 1226 + const bskyUser = await getAtprotoUser(bskyUrl, user) 1227 + if (bskyUser && bskyUser.bskyDid && !localIds.includes(bskyUser.id)) { 1228 + const serviceUrl = completeEnvironment.bskyPds.startsWith('http') 1229 + ? completeEnvironment.bskyPds 1230 + : 'https://' + completeEnvironment.bskyPds 1231 + const agent = new AtpAgent({ 1232 + service: serviceUrl 1233 + }) 1234 + const loginBskySuccess = ( 1235 + await agent.sessionManager.login({ 1236 + identifier: bskyUser.bskyDid as string, 1237 + password: pasword 1238 + }) 1239 + ).success 1240 + if (loginBskySuccess) { 1241 + // ok now time to update stuff 1242 + const newDid = bskyUser.bskyDid 1243 + bskyUser.bskyDid = `INVALID_${bskyUser.bskyDid}` 1244 + await bskyUser.save() 1245 + user.bskyDid = newDid 1246 + user.enableBsky = true 1247 + user.bskyAppPassword = pasword 1248 + await user.save() 1249 + await Post.update( 1250 + { 1251 + userId: user.id 1252 + }, 1253 + { 1254 + where: { 1255 + userId: bskyUser.id 1256 + } 1257 + } 1258 + ) 1259 + await syncBskyFollowersAndFollowing(user.id) 1260 + await forceUpdateCacheDidsAtThread() 1261 + } 1262 + } 1263 + } 1264 + }) 1265 + 1143 1266 app.get('/api/user/deleteFollow/:id', authenticateToken, async (req: AuthorizedRequest, res: Response) => { 1144 1267 const userId = req.jwtData?.userId as string 1145 1268 const forceUnfollowId = req.params?.id as string ··· 1664 1787 try { 1665 1788 // the createAccount method will also login as the newly created user. 1666 1789 const accountCreation = await agent.createAccount({ 1667 - email: `${user.url}@${completeEnvironment.instanceUrl}`, 1790 + email: user.email as string, 1668 1791 handle: `${sanitizedUrl}.${pdsHandleUrl}`, 1669 1792 password, 1670 1793 inviteCode