grain.social is a photo sharing platform built on atproto.

feat: Add XRPC procedures for all actions

- Implemented deleteGallery, deleteItem, and updateGallery endpoints for gallery management.
- Added createFollow and deleteFollow endpoints for managing follow relationships.
- Introduced photo handling APIs including uploadPhoto, deletePhoto, createExif, and applyAlts.
- Created lexicons for gallery and photo operations to define input/output schemas.
- Enhanced graph functionality with methods to retrieve followers and following with profiles.
- Added favorite creation and deletion functionalities with corresponding lexicons.

+236
__generated__/index.ts
··· 11 11 import { schemas } from './lexicons.ts' 12 12 import * as SocialGrainNotificationUpdateSeen from './types/social/grain/notification/updateSeen.ts' 13 13 import * as SocialGrainNotificationGetNotifications from './types/social/grain/notification/getNotifications.ts' 14 + import * as SocialGrainCommentDeleteComment from './types/social/grain/comment/deleteComment.ts' 15 + import * as SocialGrainCommentCreateComment from './types/social/grain/comment/createComment.ts' 16 + import * as SocialGrainGalleryDeleteGallery from './types/social/grain/gallery/deleteGallery.ts' 17 + import * as SocialGrainGalleryCreateItem from './types/social/grain/gallery/createItem.ts' 18 + import * as SocialGrainGalleryCreateGallery from './types/social/grain/gallery/createGallery.ts' 19 + import * as SocialGrainGalleryDeleteItem from './types/social/grain/gallery/deleteItem.ts' 20 + import * as SocialGrainGalleryUpdateGallery from './types/social/grain/gallery/updateGallery.ts' 21 + import * as SocialGrainGalleryApplySort from './types/social/grain/gallery/applySort.ts' 14 22 import * as SocialGrainGalleryGetGalleryThread from './types/social/grain/gallery/getGalleryThread.ts' 15 23 import * as SocialGrainGalleryGetActorGalleries from './types/social/grain/gallery/getActorGalleries.ts' 16 24 import * as SocialGrainGalleryGetGallery from './types/social/grain/gallery/getGallery.ts' 25 + import * as SocialGrainGraphDeleteFollow from './types/social/grain/graph/deleteFollow.ts' 26 + import * as SocialGrainGraphCreateFollow from './types/social/grain/graph/createFollow.ts' 17 27 import * as SocialGrainGraphGetFollowers from './types/social/grain/graph/getFollowers.ts' 18 28 import * as SocialGrainGraphGetFollows from './types/social/grain/graph/getFollows.ts' 29 + import * as SocialGrainFavoriteDeleteFavorite from './types/social/grain/favorite/deleteFavorite.ts' 30 + import * as SocialGrainFavoriteCreateFavorite from './types/social/grain/favorite/createFavorite.ts' 19 31 import * as SocialGrainFeedGetTimeline from './types/social/grain/feed/getTimeline.ts' 20 32 import * as SocialGrainActorGetProfile from './types/social/grain/actor/getProfile.ts' 21 33 import * as SocialGrainActorSearchActors from './types/social/grain/actor/searchActors.ts' 34 + import * as SocialGrainActorUpdateAvatar from './types/social/grain/actor/updateAvatar.ts' 22 35 import * as SocialGrainActorGetActorFavs from './types/social/grain/actor/getActorFavs.ts' 36 + import * as SocialGrainActorUpdateProfile from './types/social/grain/actor/updateProfile.ts' 37 + import * as SocialGrainPhotoDeletePhoto from './types/social/grain/photo/deletePhoto.ts' 38 + import * as SocialGrainPhotoUploadPhoto from './types/social/grain/photo/uploadPhoto.ts' 39 + import * as SocialGrainPhotoCreateExif from './types/social/grain/photo/createExif.ts' 40 + import * as SocialGrainPhotoApplyAlts from './types/social/grain/photo/applyAlts.ts' 23 41 import * as SocialGrainPhotoGetActorPhotos from './types/social/grain/photo/getActorPhotos.ts' 24 42 25 43 export const APP_BSKY_GRAPH = { ··· 192 210 export class SocialGrainNS { 193 211 _server: Server 194 212 notification: SocialGrainNotificationNS 213 + comment: SocialGrainCommentNS 195 214 gallery: SocialGrainGalleryNS 196 215 graph: SocialGrainGraphNS 216 + favorite: SocialGrainFavoriteNS 197 217 labeler: SocialGrainLabelerNS 198 218 feed: SocialGrainFeedNS 199 219 actor: SocialGrainActorNS ··· 202 222 constructor(server: Server) { 203 223 this._server = server 204 224 this.notification = new SocialGrainNotificationNS(server) 225 + this.comment = new SocialGrainCommentNS(server) 205 226 this.gallery = new SocialGrainGalleryNS(server) 206 227 this.graph = new SocialGrainGraphNS(server) 228 + this.favorite = new SocialGrainFavoriteNS(server) 207 229 this.labeler = new SocialGrainLabelerNS(server) 208 230 this.feed = new SocialGrainFeedNS(server) 209 231 this.actor = new SocialGrainActorNS(server) ··· 241 263 } 242 264 } 243 265 266 + export class SocialGrainCommentNS { 267 + _server: Server 268 + 269 + constructor(server: Server) { 270 + this._server = server 271 + } 272 + 273 + deleteComment<AV extends AuthVerifier>( 274 + cfg: ConfigOf< 275 + AV, 276 + SocialGrainCommentDeleteComment.Handler<ExtractAuth<AV>>, 277 + SocialGrainCommentDeleteComment.HandlerReqCtx<ExtractAuth<AV>> 278 + >, 279 + ) { 280 + const nsid = 'social.grain.comment.deleteComment' // @ts-ignore 281 + return this._server.xrpc.method(nsid, cfg) 282 + } 283 + 284 + createComment<AV extends AuthVerifier>( 285 + cfg: ConfigOf< 286 + AV, 287 + SocialGrainCommentCreateComment.Handler<ExtractAuth<AV>>, 288 + SocialGrainCommentCreateComment.HandlerReqCtx<ExtractAuth<AV>> 289 + >, 290 + ) { 291 + const nsid = 'social.grain.comment.createComment' // @ts-ignore 292 + return this._server.xrpc.method(nsid, cfg) 293 + } 294 + } 295 + 244 296 export class SocialGrainGalleryNS { 245 297 _server: Server 246 298 247 299 constructor(server: Server) { 248 300 this._server = server 301 + } 302 + 303 + deleteGallery<AV extends AuthVerifier>( 304 + cfg: ConfigOf< 305 + AV, 306 + SocialGrainGalleryDeleteGallery.Handler<ExtractAuth<AV>>, 307 + SocialGrainGalleryDeleteGallery.HandlerReqCtx<ExtractAuth<AV>> 308 + >, 309 + ) { 310 + const nsid = 'social.grain.gallery.deleteGallery' // @ts-ignore 311 + return this._server.xrpc.method(nsid, cfg) 312 + } 313 + 314 + createItem<AV extends AuthVerifier>( 315 + cfg: ConfigOf< 316 + AV, 317 + SocialGrainGalleryCreateItem.Handler<ExtractAuth<AV>>, 318 + SocialGrainGalleryCreateItem.HandlerReqCtx<ExtractAuth<AV>> 319 + >, 320 + ) { 321 + const nsid = 'social.grain.gallery.createItem' // @ts-ignore 322 + return this._server.xrpc.method(nsid, cfg) 323 + } 324 + 325 + createGallery<AV extends AuthVerifier>( 326 + cfg: ConfigOf< 327 + AV, 328 + SocialGrainGalleryCreateGallery.Handler<ExtractAuth<AV>>, 329 + SocialGrainGalleryCreateGallery.HandlerReqCtx<ExtractAuth<AV>> 330 + >, 331 + ) { 332 + const nsid = 'social.grain.gallery.createGallery' // @ts-ignore 333 + return this._server.xrpc.method(nsid, cfg) 334 + } 335 + 336 + deleteItem<AV extends AuthVerifier>( 337 + cfg: ConfigOf< 338 + AV, 339 + SocialGrainGalleryDeleteItem.Handler<ExtractAuth<AV>>, 340 + SocialGrainGalleryDeleteItem.HandlerReqCtx<ExtractAuth<AV>> 341 + >, 342 + ) { 343 + const nsid = 'social.grain.gallery.deleteItem' // @ts-ignore 344 + return this._server.xrpc.method(nsid, cfg) 345 + } 346 + 347 + updateGallery<AV extends AuthVerifier>( 348 + cfg: ConfigOf< 349 + AV, 350 + SocialGrainGalleryUpdateGallery.Handler<ExtractAuth<AV>>, 351 + SocialGrainGalleryUpdateGallery.HandlerReqCtx<ExtractAuth<AV>> 352 + >, 353 + ) { 354 + const nsid = 'social.grain.gallery.updateGallery' // @ts-ignore 355 + return this._server.xrpc.method(nsid, cfg) 356 + } 357 + 358 + applySort<AV extends AuthVerifier>( 359 + cfg: ConfigOf< 360 + AV, 361 + SocialGrainGalleryApplySort.Handler<ExtractAuth<AV>>, 362 + SocialGrainGalleryApplySort.HandlerReqCtx<ExtractAuth<AV>> 363 + >, 364 + ) { 365 + const nsid = 'social.grain.gallery.applySort' // @ts-ignore 366 + return this._server.xrpc.method(nsid, cfg) 249 367 } 250 368 251 369 getGalleryThread<AV extends AuthVerifier>( ··· 289 407 this._server = server 290 408 } 291 409 410 + deleteFollow<AV extends AuthVerifier>( 411 + cfg: ConfigOf< 412 + AV, 413 + SocialGrainGraphDeleteFollow.Handler<ExtractAuth<AV>>, 414 + SocialGrainGraphDeleteFollow.HandlerReqCtx<ExtractAuth<AV>> 415 + >, 416 + ) { 417 + const nsid = 'social.grain.graph.deleteFollow' // @ts-ignore 418 + return this._server.xrpc.method(nsid, cfg) 419 + } 420 + 421 + createFollow<AV extends AuthVerifier>( 422 + cfg: ConfigOf< 423 + AV, 424 + SocialGrainGraphCreateFollow.Handler<ExtractAuth<AV>>, 425 + SocialGrainGraphCreateFollow.HandlerReqCtx<ExtractAuth<AV>> 426 + >, 427 + ) { 428 + const nsid = 'social.grain.graph.createFollow' // @ts-ignore 429 + return this._server.xrpc.method(nsid, cfg) 430 + } 431 + 292 432 getFollowers<AV extends AuthVerifier>( 293 433 cfg: ConfigOf< 294 434 AV, ··· 312 452 } 313 453 } 314 454 455 + export class SocialGrainFavoriteNS { 456 + _server: Server 457 + 458 + constructor(server: Server) { 459 + this._server = server 460 + } 461 + 462 + deleteFavorite<AV extends AuthVerifier>( 463 + cfg: ConfigOf< 464 + AV, 465 + SocialGrainFavoriteDeleteFavorite.Handler<ExtractAuth<AV>>, 466 + SocialGrainFavoriteDeleteFavorite.HandlerReqCtx<ExtractAuth<AV>> 467 + >, 468 + ) { 469 + const nsid = 'social.grain.favorite.deleteFavorite' // @ts-ignore 470 + return this._server.xrpc.method(nsid, cfg) 471 + } 472 + 473 + createFavorite<AV extends AuthVerifier>( 474 + cfg: ConfigOf< 475 + AV, 476 + SocialGrainFavoriteCreateFavorite.Handler<ExtractAuth<AV>>, 477 + SocialGrainFavoriteCreateFavorite.HandlerReqCtx<ExtractAuth<AV>> 478 + >, 479 + ) { 480 + const nsid = 'social.grain.favorite.createFavorite' // @ts-ignore 481 + return this._server.xrpc.method(nsid, cfg) 482 + } 483 + } 484 + 315 485 export class SocialGrainLabelerNS { 316 486 _server: Server 317 487 ··· 368 538 return this._server.xrpc.method(nsid, cfg) 369 539 } 370 540 541 + updateAvatar<AV extends AuthVerifier>( 542 + cfg: ConfigOf< 543 + AV, 544 + SocialGrainActorUpdateAvatar.Handler<ExtractAuth<AV>>, 545 + SocialGrainActorUpdateAvatar.HandlerReqCtx<ExtractAuth<AV>> 546 + >, 547 + ) { 548 + const nsid = 'social.grain.actor.updateAvatar' // @ts-ignore 549 + return this._server.xrpc.method(nsid, cfg) 550 + } 551 + 371 552 getActorFavs<AV extends AuthVerifier>( 372 553 cfg: ConfigOf< 373 554 AV, ··· 376 557 >, 377 558 ) { 378 559 const nsid = 'social.grain.actor.getActorFavs' // @ts-ignore 560 + return this._server.xrpc.method(nsid, cfg) 561 + } 562 + 563 + updateProfile<AV extends AuthVerifier>( 564 + cfg: ConfigOf< 565 + AV, 566 + SocialGrainActorUpdateProfile.Handler<ExtractAuth<AV>>, 567 + SocialGrainActorUpdateProfile.HandlerReqCtx<ExtractAuth<AV>> 568 + >, 569 + ) { 570 + const nsid = 'social.grain.actor.updateProfile' // @ts-ignore 379 571 return this._server.xrpc.method(nsid, cfg) 380 572 } 381 573 } ··· 385 577 386 578 constructor(server: Server) { 387 579 this._server = server 580 + } 581 + 582 + deletePhoto<AV extends AuthVerifier>( 583 + cfg: ConfigOf< 584 + AV, 585 + SocialGrainPhotoDeletePhoto.Handler<ExtractAuth<AV>>, 586 + SocialGrainPhotoDeletePhoto.HandlerReqCtx<ExtractAuth<AV>> 587 + >, 588 + ) { 589 + const nsid = 'social.grain.photo.deletePhoto' // @ts-ignore 590 + return this._server.xrpc.method(nsid, cfg) 591 + } 592 + 593 + uploadPhoto<AV extends AuthVerifier>( 594 + cfg: ConfigOf< 595 + AV, 596 + SocialGrainPhotoUploadPhoto.Handler<ExtractAuth<AV>>, 597 + SocialGrainPhotoUploadPhoto.HandlerReqCtx<ExtractAuth<AV>> 598 + >, 599 + ) { 600 + const nsid = 'social.grain.photo.uploadPhoto' // @ts-ignore 601 + return this._server.xrpc.method(nsid, cfg) 602 + } 603 + 604 + createExif<AV extends AuthVerifier>( 605 + cfg: ConfigOf< 606 + AV, 607 + SocialGrainPhotoCreateExif.Handler<ExtractAuth<AV>>, 608 + SocialGrainPhotoCreateExif.HandlerReqCtx<ExtractAuth<AV>> 609 + >, 610 + ) { 611 + const nsid = 'social.grain.photo.createExif' // @ts-ignore 612 + return this._server.xrpc.method(nsid, cfg) 613 + } 614 + 615 + applyAlts<AV extends AuthVerifier>( 616 + cfg: ConfigOf< 617 + AV, 618 + SocialGrainPhotoApplyAlts.Handler<ExtractAuth<AV>>, 619 + SocialGrainPhotoApplyAlts.HandlerReqCtx<ExtractAuth<AV>> 620 + >, 621 + ) { 622 + const nsid = 'social.grain.photo.applyAlts' // @ts-ignore 623 + return this._server.xrpc.method(nsid, cfg) 388 624 } 389 625 390 626 getActorPhotos<AV extends AuthVerifier>(
+785 -25
__generated__/lexicons.ts
··· 2666 2666 }, 2667 2667 }, 2668 2668 }, 2669 + SocialGrainCommentDeleteComment: { 2670 + lexicon: 1, 2671 + id: 'social.grain.comment.deleteComment', 2672 + defs: { 2673 + main: { 2674 + type: 'procedure', 2675 + description: 'Delete a comment. Requires auth.', 2676 + input: { 2677 + encoding: 'application/json', 2678 + schema: { 2679 + type: 'object', 2680 + required: ['uri'], 2681 + properties: { 2682 + uri: { 2683 + type: 'string', 2684 + format: 'at-uri', 2685 + description: 'AT URI of the comment to delete', 2686 + }, 2687 + }, 2688 + }, 2689 + }, 2690 + output: { 2691 + encoding: 'application/json', 2692 + schema: { 2693 + type: 'object', 2694 + properties: { 2695 + success: { 2696 + type: 'boolean', 2697 + description: 'True if the comment was deleted', 2698 + }, 2699 + }, 2700 + }, 2701 + }, 2702 + }, 2703 + }, 2704 + }, 2705 + SocialGrainCommentCreateComment: { 2706 + lexicon: 1, 2707 + id: 'social.grain.comment.createComment', 2708 + defs: { 2709 + main: { 2710 + type: 'procedure', 2711 + description: 'Create a comment. Requires auth.', 2712 + input: { 2713 + encoding: 'application/json', 2714 + schema: { 2715 + type: 'object', 2716 + required: ['text', 'subject'], 2717 + properties: { 2718 + text: { 2719 + type: 'string', 2720 + maxLength: 3000, 2721 + maxGraphemes: 300, 2722 + }, 2723 + subject: { 2724 + type: 'string', 2725 + format: 'at-uri', 2726 + }, 2727 + focus: { 2728 + type: 'string', 2729 + format: 'at-uri', 2730 + }, 2731 + replyTo: { 2732 + type: 'string', 2733 + format: 'at-uri', 2734 + }, 2735 + }, 2736 + }, 2737 + }, 2738 + output: { 2739 + encoding: 'application/json', 2740 + schema: { 2741 + type: 'object', 2742 + properties: { 2743 + commentUri: { 2744 + type: 'string', 2745 + format: 'at-uri', 2746 + description: 'AT URI of the created comment', 2747 + }, 2748 + }, 2749 + }, 2750 + }, 2751 + }, 2752 + }, 2753 + }, 2669 2754 SocialGrainComment: { 2670 2755 lexicon: 1, 2671 2756 id: 'social.grain.comment', ··· 2712 2797 }, 2713 2798 }, 2714 2799 }, 2800 + SocialGrainGalleryDeleteGallery: { 2801 + lexicon: 1, 2802 + id: 'social.grain.gallery.deleteGallery', 2803 + defs: { 2804 + main: { 2805 + type: 'procedure', 2806 + description: 2807 + 'Delete a gallery. Does not delete the items in the gallery, just the gallery itself.', 2808 + input: { 2809 + encoding: 'application/json', 2810 + schema: { 2811 + type: 'object', 2812 + required: ['uri'], 2813 + properties: { 2814 + uri: { 2815 + type: 'string', 2816 + format: 'at-uri', 2817 + description: 'Unique identifier of the gallery to delete', 2818 + }, 2819 + }, 2820 + }, 2821 + }, 2822 + output: { 2823 + encoding: 'application/json', 2824 + schema: { 2825 + type: 'object', 2826 + properties: { 2827 + success: { 2828 + type: 'boolean', 2829 + description: 'True if the gallery was deleted', 2830 + }, 2831 + }, 2832 + }, 2833 + }, 2834 + }, 2835 + }, 2836 + }, 2715 2837 SocialGrainGalleryItem: { 2716 2838 lexicon: 1, 2717 2839 id: 'social.grain.gallery.item', ··· 2744 2866 }, 2745 2867 }, 2746 2868 }, 2869 + SocialGrainGalleryCreateItem: { 2870 + lexicon: 1, 2871 + id: 'social.grain.gallery.createItem', 2872 + defs: { 2873 + main: { 2874 + type: 'procedure', 2875 + description: 'Create a new gallery item', 2876 + input: { 2877 + encoding: 'application/json', 2878 + schema: { 2879 + type: 'object', 2880 + required: ['galleryUri', 'photoUri', 'position'], 2881 + properties: { 2882 + galleryUri: { 2883 + type: 'string', 2884 + format: 'at-uri', 2885 + description: 'AT URI of the gallery to create the item in', 2886 + }, 2887 + photoUri: { 2888 + type: 'string', 2889 + format: 'at-uri', 2890 + description: 'AT URI of the photo to be added as an item', 2891 + }, 2892 + position: { 2893 + type: 'integer', 2894 + description: 2895 + 'Position of the item in the gallery, used for ordering', 2896 + }, 2897 + }, 2898 + }, 2899 + }, 2900 + output: { 2901 + encoding: 'application/json', 2902 + schema: { 2903 + type: 'object', 2904 + properties: { 2905 + itemUri: { 2906 + type: 'string', 2907 + format: 'at-uri', 2908 + description: 'AT URI of the created gallery item', 2909 + }, 2910 + }, 2911 + }, 2912 + }, 2913 + }, 2914 + }, 2915 + }, 2916 + SocialGrainGalleryCreateGallery: { 2917 + lexicon: 1, 2918 + id: 'social.grain.gallery.createGallery', 2919 + defs: { 2920 + main: { 2921 + type: 'procedure', 2922 + description: 'Create a new gallery', 2923 + input: { 2924 + encoding: 'application/json', 2925 + schema: { 2926 + type: 'object', 2927 + required: ['title'], 2928 + properties: { 2929 + title: { 2930 + type: 'string', 2931 + maxLength: 100, 2932 + }, 2933 + description: { 2934 + type: 'string', 2935 + maxLength: 1000, 2936 + }, 2937 + }, 2938 + }, 2939 + }, 2940 + output: { 2941 + encoding: 'application/json', 2942 + schema: { 2943 + type: 'object', 2944 + properties: { 2945 + galleryUri: { 2946 + type: 'string', 2947 + format: 'at-uri', 2948 + description: 'AT URI of the created gallery', 2949 + }, 2950 + }, 2951 + }, 2952 + }, 2953 + }, 2954 + }, 2955 + }, 2747 2956 SocialGrainGalleryDefs: { 2748 2957 lexicon: 1, 2749 2958 id: 'social.grain.gallery.defs', ··· 2837 3046 }, 2838 3047 }, 2839 3048 }, 3049 + SocialGrainGalleryDeleteItem: { 3050 + lexicon: 1, 3051 + id: 'social.grain.gallery.deleteItem', 3052 + defs: { 3053 + main: { 3054 + type: 'procedure', 3055 + description: 'Delete a gallery item', 3056 + input: { 3057 + encoding: 'application/json', 3058 + schema: { 3059 + type: 'object', 3060 + required: ['uri'], 3061 + properties: { 3062 + uri: { 3063 + type: 'string', 3064 + format: 'at-uri', 3065 + description: 'AT URI of the gallery to create the item in', 3066 + }, 3067 + }, 3068 + }, 3069 + }, 3070 + output: { 3071 + encoding: 'application/json', 3072 + schema: { 3073 + type: 'object', 3074 + properties: { 3075 + success: { 3076 + type: 'boolean', 3077 + description: 'True if the gallery item was deleted', 3078 + }, 3079 + }, 3080 + }, 3081 + }, 3082 + }, 3083 + }, 3084 + }, 2840 3085 SocialGrainGallery: { 2841 3086 lexicon: 1, 2842 3087 id: 'social.grain.gallery', ··· 2884 3129 }, 2885 3130 }, 2886 3131 }, 3132 + SocialGrainGalleryUpdateGallery: { 3133 + lexicon: 1, 3134 + id: 'social.grain.gallery.updateGallery', 3135 + defs: { 3136 + main: { 3137 + type: 'procedure', 3138 + description: 'Create a new gallery', 3139 + input: { 3140 + encoding: 'application/json', 3141 + schema: { 3142 + type: 'object', 3143 + required: ['galleryUri', 'title'], 3144 + properties: { 3145 + galleryUri: { 3146 + type: 'string', 3147 + format: 'at-uri', 3148 + description: 'The AT-URI of the gallery to update', 3149 + }, 3150 + title: { 3151 + type: 'string', 3152 + }, 3153 + description: { 3154 + type: 'string', 3155 + }, 3156 + }, 3157 + }, 3158 + }, 3159 + output: { 3160 + encoding: 'application/json', 3161 + schema: { 3162 + type: 'object', 3163 + properties: { 3164 + success: { 3165 + type: 'boolean', 3166 + description: 'True if the gallery was updated', 3167 + }, 3168 + }, 3169 + }, 3170 + }, 3171 + }, 3172 + }, 3173 + }, 3174 + SocialGrainGalleryApplySort: { 3175 + lexicon: 1, 3176 + id: 'social.grain.gallery.applySort', 3177 + defs: { 3178 + main: { 3179 + type: 'procedure', 3180 + description: 'Apply sorting to photos in a gallery. Requires auth.', 3181 + input: { 3182 + encoding: 'application/json', 3183 + schema: { 3184 + type: 'object', 3185 + required: ['writes'], 3186 + properties: { 3187 + writes: { 3188 + type: 'array', 3189 + items: { 3190 + type: 'ref', 3191 + ref: 'lex:social.grain.gallery.applySort#update', 3192 + }, 3193 + }, 3194 + }, 3195 + }, 3196 + }, 3197 + output: { 3198 + encoding: 'application/json', 3199 + schema: { 3200 + type: 'object', 3201 + properties: { 3202 + success: { 3203 + type: 'boolean', 3204 + description: 'True if the writes were successfully applied', 3205 + }, 3206 + }, 3207 + }, 3208 + }, 3209 + }, 3210 + update: { 3211 + type: 'object', 3212 + required: ['itemUri', 'position'], 3213 + properties: { 3214 + itemUri: { 3215 + type: 'string', 3216 + format: 'at-uri', 3217 + description: 'AT URI of the item to update', 3218 + }, 3219 + position: { 3220 + type: 'integer', 3221 + description: 3222 + 'The position of the item in the gallery, used for ordering', 3223 + }, 3224 + }, 3225 + }, 3226 + }, 3227 + }, 2887 3228 SocialGrainGalleryGetGalleryThread: { 2888 3229 lexicon: 1, 2889 3230 id: 'social.grain.gallery.getGalleryThread', ··· 3006 3347 }, 3007 3348 }, 3008 3349 }, 3350 + SocialGrainGraphDeleteFollow: { 3351 + lexicon: 1, 3352 + id: 'social.grain.graph.deleteFollow', 3353 + defs: { 3354 + main: { 3355 + type: 'procedure', 3356 + description: 'Delete a follow relationship. Requires auth.', 3357 + input: { 3358 + encoding: 'application/json', 3359 + schema: { 3360 + type: 'object', 3361 + required: ['uri'], 3362 + properties: { 3363 + uri: { 3364 + type: 'string', 3365 + format: 'at-uri', 3366 + description: 'AT URI of the follow record to delete', 3367 + }, 3368 + }, 3369 + }, 3370 + }, 3371 + output: { 3372 + encoding: 'application/json', 3373 + schema: { 3374 + type: 'object', 3375 + properties: { 3376 + success: { 3377 + type: 'boolean', 3378 + description: 'True if the follow was deleted', 3379 + }, 3380 + }, 3381 + }, 3382 + }, 3383 + }, 3384 + }, 3385 + }, 3009 3386 SocialGrainGraphFollow: { 3010 3387 lexicon: 1, 3011 3388 id: 'social.grain.graph.follow', ··· 3030 3407 }, 3031 3408 }, 3032 3409 }, 3410 + SocialGrainGraphCreateFollow: { 3411 + lexicon: 1, 3412 + id: 'social.grain.graph.createFollow', 3413 + defs: { 3414 + main: { 3415 + type: 'procedure', 3416 + description: 'Create a follow relationship between actors.', 3417 + input: { 3418 + encoding: 'application/json', 3419 + schema: { 3420 + type: 'object', 3421 + required: ['subject'], 3422 + properties: { 3423 + subject: { 3424 + type: 'string', 3425 + format: 'at-identifier', 3426 + description: 'DID of the actor to follow.', 3427 + }, 3428 + }, 3429 + }, 3430 + }, 3431 + output: { 3432 + encoding: 'application/json', 3433 + schema: { 3434 + type: 'object', 3435 + properties: { 3436 + followUri: { 3437 + type: 'string', 3438 + format: 'at-uri', 3439 + description: 'AT URI of the created follow record.', 3440 + }, 3441 + }, 3442 + }, 3443 + }, 3444 + }, 3445 + }, 3446 + }, 3033 3447 SocialGrainGraphGetFollowers: { 3034 3448 lexicon: 1, 3035 3449 id: 'social.grain.graph.getFollowers', ··· 3136 3550 }, 3137 3551 }, 3138 3552 }, 3553 + SocialGrainFavoriteDeleteFavorite: { 3554 + lexicon: 1, 3555 + id: 'social.grain.favorite.deleteFavorite', 3556 + defs: { 3557 + main: { 3558 + type: 'procedure', 3559 + description: 'Delete a favorite item by its ID.', 3560 + input: { 3561 + encoding: 'application/json', 3562 + schema: { 3563 + type: 'object', 3564 + required: ['uri'], 3565 + properties: { 3566 + uri: { 3567 + type: 'string', 3568 + format: 'at-uri', 3569 + description: 'The AT URI of the favorite to delete.', 3570 + }, 3571 + }, 3572 + }, 3573 + }, 3574 + output: { 3575 + encoding: 'application/json', 3576 + schema: { 3577 + type: 'object', 3578 + required: ['success'], 3579 + properties: { 3580 + success: { 3581 + type: 'boolean', 3582 + description: 3583 + 'Indicates if the favorite was successfully deleted.', 3584 + }, 3585 + }, 3586 + }, 3587 + }, 3588 + }, 3589 + }, 3590 + }, 3591 + SocialGrainFavoriteCreateFavorite: { 3592 + lexicon: 1, 3593 + id: 'social.grain.favorite.createFavorite', 3594 + defs: { 3595 + main: { 3596 + description: 'Create a favorite for a given subject.', 3597 + type: 'procedure', 3598 + input: { 3599 + encoding: 'application/json', 3600 + schema: { 3601 + type: 'object', 3602 + required: ['subject'], 3603 + properties: { 3604 + subject: { 3605 + type: 'string', 3606 + format: 'at-uri', 3607 + description: 'URI of the subject to favorite.', 3608 + }, 3609 + }, 3610 + }, 3611 + }, 3612 + output: { 3613 + encoding: 'application/json', 3614 + schema: { 3615 + type: 'object', 3616 + required: ['favoriteUri'], 3617 + properties: { 3618 + favoriteUri: { 3619 + type: 'string', 3620 + format: 'at-uri', 3621 + description: 'AT URI for the created favorite.', 3622 + }, 3623 + }, 3624 + }, 3625 + }, 3626 + }, 3627 + }, 3628 + }, 3629 + SocialGrainFavorite: { 3630 + lexicon: 1, 3631 + id: 'social.grain.favorite', 3632 + defs: { 3633 + main: { 3634 + type: 'record', 3635 + key: 'tid', 3636 + record: { 3637 + type: 'object', 3638 + required: ['createdAt', 'subject'], 3639 + properties: { 3640 + createdAt: { 3641 + type: 'string', 3642 + format: 'datetime', 3643 + }, 3644 + subject: { 3645 + type: 'string', 3646 + format: 'at-uri', 3647 + }, 3648 + }, 3649 + }, 3650 + }, 3651 + }, 3652 + }, 3139 3653 SocialGrainLabelerDefs: { 3140 3654 lexicon: 1, 3141 3655 id: 'social.grain.labeler.defs', ··· 3384 3898 }, 3385 3899 }, 3386 3900 }, 3387 - SocialGrainFavorite: { 3388 - lexicon: 1, 3389 - id: 'social.grain.favorite', 3390 - defs: { 3391 - main: { 3392 - type: 'record', 3393 - key: 'tid', 3394 - record: { 3395 - type: 'object', 3396 - required: ['createdAt', 'subject'], 3397 - properties: { 3398 - createdAt: { 3399 - type: 'string', 3400 - format: 'datetime', 3401 - }, 3402 - subject: { 3403 - type: 'string', 3404 - format: 'at-uri', 3405 - }, 3406 - }, 3407 - }, 3408 - }, 3409 - }, 3410 - }, 3411 3901 SocialGrainActorDefs: { 3412 3902 lexicon: 1, 3413 3903 id: 'social.grain.actor.defs', ··· 3618 4108 }, 3619 4109 }, 3620 4110 }, 4111 + SocialGrainActorUpdateAvatar: { 4112 + lexicon: 1, 4113 + id: 'social.grain.actor.updateAvatar', 4114 + defs: { 4115 + main: { 4116 + type: 'procedure', 4117 + description: "Update an actor's avatar. Requires auth.", 4118 + input: { 4119 + encoding: '*/*', 4120 + }, 4121 + output: { 4122 + encoding: 'application/json', 4123 + schema: { 4124 + type: 'object', 4125 + properties: { 4126 + success: { 4127 + type: 'boolean', 4128 + description: 4129 + 'Indicates whether the avatar update was successful.', 4130 + }, 4131 + }, 4132 + }, 4133 + }, 4134 + }, 4135 + }, 4136 + }, 3621 4137 SocialGrainActorGetActorFavs: { 3622 4138 lexicon: 1, 3623 4139 id: 'social.grain.actor.getActorFavs', ··· 3705 4221 }, 3706 4222 }, 3707 4223 }, 4224 + SocialGrainActorUpdateProfile: { 4225 + lexicon: 1, 4226 + id: 'social.grain.actor.updateProfile', 4227 + defs: { 4228 + main: { 4229 + type: 'procedure', 4230 + description: "Update an actor's profile info. Requires auth.", 4231 + input: { 4232 + encoding: 'application/json', 4233 + schema: { 4234 + type: 'object', 4235 + properties: { 4236 + displayName: { 4237 + type: 'string', 4238 + maxGraphemes: 64, 4239 + maxLength: 640, 4240 + }, 4241 + description: { 4242 + type: 'string', 4243 + description: 'Free-form profile description text.', 4244 + maxGraphemes: 256, 4245 + maxLength: 2560, 4246 + }, 4247 + }, 4248 + }, 4249 + }, 4250 + output: { 4251 + encoding: 'application/json', 4252 + schema: { 4253 + type: 'object', 4254 + properties: { 4255 + success: { 4256 + type: 'boolean', 4257 + description: 4258 + 'Indicates whether the profile update was successful.', 4259 + }, 4260 + }, 4261 + }, 4262 + }, 4263 + }, 4264 + }, 4265 + }, 3708 4266 SocialGrainPhotoDefs: { 3709 4267 lexicon: 1, 3710 4268 id: 'social.grain.photo.defs', ··· 3826 4384 }, 3827 4385 }, 3828 4386 }, 4387 + SocialGrainPhotoDeletePhoto: { 4388 + lexicon: 1, 4389 + id: 'social.grain.photo.deletePhoto', 4390 + defs: { 4391 + main: { 4392 + type: 'procedure', 4393 + description: 'Delete a favorite photo by its unique at-uri.', 4394 + input: { 4395 + encoding: 'application/json', 4396 + schema: { 4397 + type: 'object', 4398 + required: ['uri'], 4399 + properties: { 4400 + uri: { 4401 + type: 'string', 4402 + format: 'at-uri', 4403 + description: 'AT URI of the photo to delete.', 4404 + }, 4405 + }, 4406 + }, 4407 + }, 4408 + output: { 4409 + encoding: 'application/json', 4410 + schema: { 4411 + type: 'object', 4412 + required: ['success'], 4413 + properties: { 4414 + success: { 4415 + type: 'boolean', 4416 + description: 'Indicates if the photo was successfully deleted.', 4417 + }, 4418 + }, 4419 + }, 4420 + }, 4421 + }, 4422 + }, 4423 + }, 4424 + SocialGrainPhotoUploadPhoto: { 4425 + lexicon: 1, 4426 + id: 'social.grain.photo.uploadPhoto', 4427 + defs: { 4428 + main: { 4429 + type: 'procedure', 4430 + description: 'Upload a photo. Requires auth.', 4431 + input: { 4432 + encoding: '*/*', 4433 + }, 4434 + output: { 4435 + encoding: 'application/json', 4436 + schema: { 4437 + type: 'object', 4438 + properties: { 4439 + photoUri: { 4440 + type: 'string', 4441 + format: 'at-uri', 4442 + description: 'AT URI of the created photo', 4443 + }, 4444 + }, 4445 + }, 4446 + }, 4447 + }, 4448 + }, 4449 + }, 4450 + SocialGrainPhotoCreateExif: { 4451 + lexicon: 1, 4452 + id: 'social.grain.photo.createExif', 4453 + defs: { 4454 + main: { 4455 + type: 'procedure', 4456 + description: 'Create a new Exif record for a photo', 4457 + input: { 4458 + encoding: 'application/json', 4459 + schema: { 4460 + type: 'object', 4461 + required: ['photo'], 4462 + properties: { 4463 + photo: { 4464 + type: 'string', 4465 + format: 'at-uri', 4466 + }, 4467 + dateTimeOriginal: { 4468 + type: 'string', 4469 + format: 'datetime', 4470 + }, 4471 + exposureTime: { 4472 + type: 'integer', 4473 + }, 4474 + fNumber: { 4475 + type: 'integer', 4476 + }, 4477 + flash: { 4478 + type: 'string', 4479 + }, 4480 + focalLengthIn35mmFormat: { 4481 + type: 'integer', 4482 + }, 4483 + iSO: { 4484 + type: 'integer', 4485 + }, 4486 + lensMake: { 4487 + type: 'string', 4488 + }, 4489 + lensModel: { 4490 + type: 'string', 4491 + }, 4492 + make: { 4493 + type: 'string', 4494 + }, 4495 + model: { 4496 + type: 'string', 4497 + }, 4498 + }, 4499 + }, 4500 + }, 4501 + output: { 4502 + encoding: 'application/json', 4503 + schema: { 4504 + type: 'object', 4505 + properties: { 4506 + exifUri: { 4507 + type: 'string', 4508 + format: 'at-uri', 4509 + description: 'AT URI of the created gallery', 4510 + }, 4511 + }, 4512 + }, 4513 + }, 4514 + }, 4515 + }, 4516 + }, 3829 4517 SocialGrainPhotoExif: { 3830 4518 lexicon: 1, 3831 4519 id: 'social.grain.photo.exif', ··· 3878 4566 model: { 3879 4567 type: 'string', 3880 4568 }, 4569 + }, 4570 + }, 4571 + }, 4572 + }, 4573 + }, 4574 + SocialGrainPhotoApplyAlts: { 4575 + lexicon: 1, 4576 + id: 'social.grain.photo.applyAlts', 4577 + defs: { 4578 + main: { 4579 + type: 'procedure', 4580 + description: 'Apply alt texts to photos in a gallery. Requires auth.', 4581 + input: { 4582 + encoding: 'application/json', 4583 + schema: { 4584 + type: 'object', 4585 + required: ['writes'], 4586 + properties: { 4587 + writes: { 4588 + type: 'array', 4589 + items: { 4590 + type: 'ref', 4591 + ref: 'lex:social.grain.photo.applyAlts#update', 4592 + }, 4593 + }, 4594 + }, 4595 + }, 4596 + }, 4597 + output: { 4598 + encoding: 'application/json', 4599 + schema: { 4600 + type: 'object', 4601 + properties: { 4602 + success: { 4603 + type: 'boolean', 4604 + description: 'True if the writes were successfully applied', 4605 + }, 4606 + }, 4607 + }, 4608 + }, 4609 + }, 4610 + update: { 4611 + type: 'object', 4612 + required: ['photoUri', 'alt'], 4613 + properties: { 4614 + photoUri: { 4615 + type: 'string', 4616 + format: 'at-uri', 4617 + description: 'AT URI of the item to update', 4618 + }, 4619 + alt: { 4620 + type: 'string', 4621 + maxLength: 1000, 4622 + description: 'The alt text to apply to the photo', 3881 4623 }, 3882 4624 }, 3883 4625 }, ··· 4273 5015 SocialGrainNotificationGetNotifications: 4274 5016 'social.grain.notification.getNotifications', 4275 5017 SocialGrainCommentDefs: 'social.grain.comment.defs', 5018 + SocialGrainCommentDeleteComment: 'social.grain.comment.deleteComment', 5019 + SocialGrainCommentCreateComment: 'social.grain.comment.createComment', 4276 5020 SocialGrainComment: 'social.grain.comment', 5021 + SocialGrainGalleryDeleteGallery: 'social.grain.gallery.deleteGallery', 4277 5022 SocialGrainGalleryItem: 'social.grain.gallery.item', 5023 + SocialGrainGalleryCreateItem: 'social.grain.gallery.createItem', 5024 + SocialGrainGalleryCreateGallery: 'social.grain.gallery.createGallery', 4278 5025 SocialGrainGalleryDefs: 'social.grain.gallery.defs', 5026 + SocialGrainGalleryDeleteItem: 'social.grain.gallery.deleteItem', 4279 5027 SocialGrainGallery: 'social.grain.gallery', 5028 + SocialGrainGalleryUpdateGallery: 'social.grain.gallery.updateGallery', 5029 + SocialGrainGalleryApplySort: 'social.grain.gallery.applySort', 4280 5030 SocialGrainGalleryGetGalleryThread: 'social.grain.gallery.getGalleryThread', 4281 5031 SocialGrainGalleryGetActorGalleries: 'social.grain.gallery.getActorGalleries', 4282 5032 SocialGrainGalleryGetGallery: 'social.grain.gallery.getGallery', 5033 + SocialGrainGraphDeleteFollow: 'social.grain.graph.deleteFollow', 4283 5034 SocialGrainGraphFollow: 'social.grain.graph.follow', 5035 + SocialGrainGraphCreateFollow: 'social.grain.graph.createFollow', 4284 5036 SocialGrainGraphGetFollowers: 'social.grain.graph.getFollowers', 4285 5037 SocialGrainGraphGetFollows: 'social.grain.graph.getFollows', 5038 + SocialGrainFavoriteDeleteFavorite: 'social.grain.favorite.deleteFavorite', 5039 + SocialGrainFavoriteCreateFavorite: 'social.grain.favorite.createFavorite', 5040 + SocialGrainFavorite: 'social.grain.favorite', 4286 5041 SocialGrainLabelerDefs: 'social.grain.labeler.defs', 4287 5042 SocialGrainLabelerService: 'social.grain.labeler.service', 4288 5043 SocialGrainFeedGetTimeline: 'social.grain.feed.getTimeline', 4289 - SocialGrainFavorite: 'social.grain.favorite', 4290 5044 SocialGrainActorDefs: 'social.grain.actor.defs', 4291 5045 SocialGrainActorGetProfile: 'social.grain.actor.getProfile', 4292 5046 SocialGrainActorSearchActors: 'social.grain.actor.searchActors', 5047 + SocialGrainActorUpdateAvatar: 'social.grain.actor.updateAvatar', 4293 5048 SocialGrainActorGetActorFavs: 'social.grain.actor.getActorFavs', 4294 5049 SocialGrainActorProfile: 'social.grain.actor.profile', 5050 + SocialGrainActorUpdateProfile: 'social.grain.actor.updateProfile', 4295 5051 SocialGrainPhotoDefs: 'social.grain.photo.defs', 5052 + SocialGrainPhotoDeletePhoto: 'social.grain.photo.deletePhoto', 5053 + SocialGrainPhotoUploadPhoto: 'social.grain.photo.uploadPhoto', 5054 + SocialGrainPhotoCreateExif: 'social.grain.photo.createExif', 4296 5055 SocialGrainPhotoExif: 'social.grain.photo.exif', 5056 + SocialGrainPhotoApplyAlts: 'social.grain.photo.applyAlts', 4297 5057 SocialGrainPhotoGetActorPhotos: 'social.grain.photo.getActorPhotos', 4298 5058 SocialGrainPhoto: 'social.grain.photo', 4299 5059 ComAtprotoLabelDefs: 'com.atproto.label.defs',
+50
__generated__/types/social/grain/actor/updateAvatar.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import stream from "node:stream"; 5 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 6 + import express from "npm:express"; 7 + import { validate as _validate } from "../../../../lexicons.ts"; 8 + import { is$typed as _is$typed } from "../../../../util.ts"; 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate; 12 + const id = "social.grain.actor.updateAvatar"; 13 + 14 + export interface QueryParams {} 15 + 16 + export type InputSchema = string | Uint8Array | Blob; 17 + 18 + export interface OutputSchema { 19 + /** Indicates whether the avatar update was successful. */ 20 + success?: boolean; 21 + } 22 + 23 + export interface HandlerInput { 24 + encoding: "*/*"; 25 + body: stream.Readable; 26 + } 27 + 28 + export interface HandlerSuccess { 29 + encoding: "application/json"; 30 + body: OutputSchema; 31 + headers?: { [key: string]: string }; 32 + } 33 + 34 + export interface HandlerError { 35 + status: number; 36 + message?: string; 37 + } 38 + 39 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 40 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 41 + auth: HA; 42 + params: QueryParams; 43 + input: HandlerInput; 44 + req: express.Request; 45 + res: express.Response; 46 + resetRouteRateLimits: () => Promise<void>; 47 + }; 48 + export type Handler<HA extends HandlerAuth = never> = ( 49 + ctx: HandlerReqCtx<HA>, 50 + ) => Promise<HandlerOutput> | HandlerOutput;
+53
__generated__/types/social/grain/actor/updateProfile.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.actor.updateProfile"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + displayName?: string; 17 + /** Free-form profile description text. */ 18 + description?: string; 19 + } 20 + 21 + export interface OutputSchema { 22 + /** Indicates whether the profile update was successful. */ 23 + success?: boolean; 24 + } 25 + 26 + export interface HandlerInput { 27 + encoding: "application/json"; 28 + body: InputSchema; 29 + } 30 + 31 + export interface HandlerSuccess { 32 + encoding: "application/json"; 33 + body: OutputSchema; 34 + headers?: { [key: string]: string }; 35 + } 36 + 37 + export interface HandlerError { 38 + status: number; 39 + message?: string; 40 + } 41 + 42 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 43 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 44 + auth: HA; 45 + params: QueryParams; 46 + input: HandlerInput; 47 + req: express.Request; 48 + res: express.Response; 49 + resetRouteRateLimits: () => Promise<void>; 50 + }; 51 + export type Handler<HA extends HandlerAuth = never> = ( 52 + ctx: HandlerReqCtx<HA>, 53 + ) => Promise<HandlerOutput> | HandlerOutput;
+54
__generated__/types/social/grain/comment/createComment.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.comment.createComment"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + text: string; 17 + subject: string; 18 + focus?: string; 19 + replyTo?: string; 20 + } 21 + 22 + export interface OutputSchema { 23 + /** AT URI of the created comment */ 24 + commentUri?: string; 25 + } 26 + 27 + export interface HandlerInput { 28 + encoding: "application/json"; 29 + body: InputSchema; 30 + } 31 + 32 + export interface HandlerSuccess { 33 + encoding: "application/json"; 34 + body: OutputSchema; 35 + headers?: { [key: string]: string }; 36 + } 37 + 38 + export interface HandlerError { 39 + status: number; 40 + message?: string; 41 + } 42 + 43 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 44 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 45 + auth: HA; 46 + params: QueryParams; 47 + input: HandlerInput; 48 + req: express.Request; 49 + res: express.Response; 50 + resetRouteRateLimits: () => Promise<void>; 51 + }; 52 + export type Handler<HA extends HandlerAuth = never> = ( 53 + ctx: HandlerReqCtx<HA>, 54 + ) => Promise<HandlerOutput> | HandlerOutput;
+52
__generated__/types/social/grain/comment/deleteComment.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.comment.deleteComment"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + /** AT URI of the comment to delete */ 17 + uri: string; 18 + } 19 + 20 + export interface OutputSchema { 21 + /** True if the comment was deleted */ 22 + success?: boolean; 23 + } 24 + 25 + export interface HandlerInput { 26 + encoding: "application/json"; 27 + body: InputSchema; 28 + } 29 + 30 + export interface HandlerSuccess { 31 + encoding: "application/json"; 32 + body: OutputSchema; 33 + headers?: { [key: string]: string }; 34 + } 35 + 36 + export interface HandlerError { 37 + status: number; 38 + message?: string; 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA; 44 + params: QueryParams; 45 + input: HandlerInput; 46 + req: express.Request; 47 + res: express.Response; 48 + resetRouteRateLimits: () => Promise<void>; 49 + }; 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput;
+52
__generated__/types/social/grain/favorite/createFavorite.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.favorite.createFavorite"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + /** URI of the subject to favorite. */ 17 + subject: string; 18 + } 19 + 20 + export interface OutputSchema { 21 + /** AT URI for the created favorite. */ 22 + favoriteUri: string; 23 + } 24 + 25 + export interface HandlerInput { 26 + encoding: "application/json"; 27 + body: InputSchema; 28 + } 29 + 30 + export interface HandlerSuccess { 31 + encoding: "application/json"; 32 + body: OutputSchema; 33 + headers?: { [key: string]: string }; 34 + } 35 + 36 + export interface HandlerError { 37 + status: number; 38 + message?: string; 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA; 44 + params: QueryParams; 45 + input: HandlerInput; 46 + req: express.Request; 47 + res: express.Response; 48 + resetRouteRateLimits: () => Promise<void>; 49 + }; 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput;
+52
__generated__/types/social/grain/favorite/deleteFavorite.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.favorite.deleteFavorite"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + /** The AT URI of the favorite to delete. */ 17 + uri: string; 18 + } 19 + 20 + export interface OutputSchema { 21 + /** Indicates if the favorite was successfully deleted. */ 22 + success: boolean; 23 + } 24 + 25 + export interface HandlerInput { 26 + encoding: "application/json"; 27 + body: InputSchema; 28 + } 29 + 30 + export interface HandlerSuccess { 31 + encoding: "application/json"; 32 + body: OutputSchema; 33 + headers?: { [key: string]: string }; 34 + } 35 + 36 + export interface HandlerError { 37 + status: number; 38 + message?: string; 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA; 44 + params: QueryParams; 45 + input: HandlerInput; 46 + req: express.Request; 47 + res: express.Response; 48 + resetRouteRateLimits: () => Promise<void>; 49 + }; 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput;
+69
__generated__/types/social/grain/gallery/applySort.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.gallery.applySort"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + writes: Update[]; 17 + } 18 + 19 + export interface OutputSchema { 20 + /** True if the writes were successfully applied */ 21 + success?: boolean; 22 + } 23 + 24 + export interface HandlerInput { 25 + encoding: "application/json"; 26 + body: InputSchema; 27 + } 28 + 29 + export interface HandlerSuccess { 30 + encoding: "application/json"; 31 + body: OutputSchema; 32 + headers?: { [key: string]: string }; 33 + } 34 + 35 + export interface HandlerError { 36 + status: number; 37 + message?: string; 38 + } 39 + 40 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 41 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 42 + auth: HA; 43 + params: QueryParams; 44 + input: HandlerInput; 45 + req: express.Request; 46 + res: express.Response; 47 + resetRouteRateLimits: () => Promise<void>; 48 + }; 49 + export type Handler<HA extends HandlerAuth = never> = ( 50 + ctx: HandlerReqCtx<HA>, 51 + ) => Promise<HandlerOutput> | HandlerOutput; 52 + 53 + export interface Update { 54 + $type?: "social.grain.gallery.applySort#update"; 55 + /** AT URI of the item to update */ 56 + itemUri: string; 57 + /** The position of the item in the gallery, used for ordering */ 58 + position: number; 59 + } 60 + 61 + const hashUpdate = "update"; 62 + 63 + export function isUpdate<V>(v: V) { 64 + return is$typed(v, id, hashUpdate); 65 + } 66 + 67 + export function validateUpdate<V>(v: V) { 68 + return validate<Update & V>(v, id, hashUpdate); 69 + }
+52
__generated__/types/social/grain/gallery/createGallery.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.gallery.createGallery"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + title: string; 17 + description?: string; 18 + } 19 + 20 + export interface OutputSchema { 21 + /** AT URI of the created gallery */ 22 + galleryUri?: string; 23 + } 24 + 25 + export interface HandlerInput { 26 + encoding: "application/json"; 27 + body: InputSchema; 28 + } 29 + 30 + export interface HandlerSuccess { 31 + encoding: "application/json"; 32 + body: OutputSchema; 33 + headers?: { [key: string]: string }; 34 + } 35 + 36 + export interface HandlerError { 37 + status: number; 38 + message?: string; 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA; 44 + params: QueryParams; 45 + input: HandlerInput; 46 + req: express.Request; 47 + res: express.Response; 48 + resetRouteRateLimits: () => Promise<void>; 49 + }; 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput;
+56
__generated__/types/social/grain/gallery/createItem.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.gallery.createItem"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + /** AT URI of the gallery to create the item in */ 17 + galleryUri: string; 18 + /** AT URI of the photo to be added as an item */ 19 + photoUri: string; 20 + /** Position of the item in the gallery, used for ordering */ 21 + position: number; 22 + } 23 + 24 + export interface OutputSchema { 25 + /** AT URI of the created gallery item */ 26 + itemUri?: string; 27 + } 28 + 29 + export interface HandlerInput { 30 + encoding: "application/json"; 31 + body: InputSchema; 32 + } 33 + 34 + export interface HandlerSuccess { 35 + encoding: "application/json"; 36 + body: OutputSchema; 37 + headers?: { [key: string]: string }; 38 + } 39 + 40 + export interface HandlerError { 41 + status: number; 42 + message?: string; 43 + } 44 + 45 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 46 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 47 + auth: HA; 48 + params: QueryParams; 49 + input: HandlerInput; 50 + req: express.Request; 51 + res: express.Response; 52 + resetRouteRateLimits: () => Promise<void>; 53 + }; 54 + export type Handler<HA extends HandlerAuth = never> = ( 55 + ctx: HandlerReqCtx<HA>, 56 + ) => Promise<HandlerOutput> | HandlerOutput;
+52
__generated__/types/social/grain/gallery/deleteGallery.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.gallery.deleteGallery"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + /** Unique identifier of the gallery to delete */ 17 + uri: string; 18 + } 19 + 20 + export interface OutputSchema { 21 + /** True if the gallery was deleted */ 22 + success?: boolean; 23 + } 24 + 25 + export interface HandlerInput { 26 + encoding: "application/json"; 27 + body: InputSchema; 28 + } 29 + 30 + export interface HandlerSuccess { 31 + encoding: "application/json"; 32 + body: OutputSchema; 33 + headers?: { [key: string]: string }; 34 + } 35 + 36 + export interface HandlerError { 37 + status: number; 38 + message?: string; 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA; 44 + params: QueryParams; 45 + input: HandlerInput; 46 + req: express.Request; 47 + res: express.Response; 48 + resetRouteRateLimits: () => Promise<void>; 49 + }; 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput;
+52
__generated__/types/social/grain/gallery/deleteItem.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.gallery.deleteItem"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + /** AT URI of the gallery to create the item in */ 17 + uri: string; 18 + } 19 + 20 + export interface OutputSchema { 21 + /** True if the gallery item was deleted */ 22 + success?: boolean; 23 + } 24 + 25 + export interface HandlerInput { 26 + encoding: "application/json"; 27 + body: InputSchema; 28 + } 29 + 30 + export interface HandlerSuccess { 31 + encoding: "application/json"; 32 + body: OutputSchema; 33 + headers?: { [key: string]: string }; 34 + } 35 + 36 + export interface HandlerError { 37 + status: number; 38 + message?: string; 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA; 44 + params: QueryParams; 45 + input: HandlerInput; 46 + req: express.Request; 47 + res: express.Response; 48 + resetRouteRateLimits: () => Promise<void>; 49 + }; 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput;
+54
__generated__/types/social/grain/gallery/updateGallery.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.gallery.updateGallery"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + /** The AT-URI of the gallery to update */ 17 + galleryUri: string; 18 + title: string; 19 + description?: string; 20 + } 21 + 22 + export interface OutputSchema { 23 + /** True if the gallery was updated */ 24 + success?: boolean; 25 + } 26 + 27 + export interface HandlerInput { 28 + encoding: "application/json"; 29 + body: InputSchema; 30 + } 31 + 32 + export interface HandlerSuccess { 33 + encoding: "application/json"; 34 + body: OutputSchema; 35 + headers?: { [key: string]: string }; 36 + } 37 + 38 + export interface HandlerError { 39 + status: number; 40 + message?: string; 41 + } 42 + 43 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 44 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 45 + auth: HA; 46 + params: QueryParams; 47 + input: HandlerInput; 48 + req: express.Request; 49 + res: express.Response; 50 + resetRouteRateLimits: () => Promise<void>; 51 + }; 52 + export type Handler<HA extends HandlerAuth = never> = ( 53 + ctx: HandlerReqCtx<HA>, 54 + ) => Promise<HandlerOutput> | HandlerOutput;
+52
__generated__/types/social/grain/graph/createFollow.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.graph.createFollow"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + /** DID of the actor to follow. */ 17 + subject: string; 18 + } 19 + 20 + export interface OutputSchema { 21 + /** AT URI of the created follow record. */ 22 + followUri?: string; 23 + } 24 + 25 + export interface HandlerInput { 26 + encoding: "application/json"; 27 + body: InputSchema; 28 + } 29 + 30 + export interface HandlerSuccess { 31 + encoding: "application/json"; 32 + body: OutputSchema; 33 + headers?: { [key: string]: string }; 34 + } 35 + 36 + export interface HandlerError { 37 + status: number; 38 + message?: string; 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA; 44 + params: QueryParams; 45 + input: HandlerInput; 46 + req: express.Request; 47 + res: express.Response; 48 + resetRouteRateLimits: () => Promise<void>; 49 + }; 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput;
+52
__generated__/types/social/grain/graph/deleteFollow.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.graph.deleteFollow"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + /** AT URI of the follow record to delete */ 17 + uri: string; 18 + } 19 + 20 + export interface OutputSchema { 21 + /** True if the follow was deleted */ 22 + success?: boolean; 23 + } 24 + 25 + export interface HandlerInput { 26 + encoding: "application/json"; 27 + body: InputSchema; 28 + } 29 + 30 + export interface HandlerSuccess { 31 + encoding: "application/json"; 32 + body: OutputSchema; 33 + headers?: { [key: string]: string }; 34 + } 35 + 36 + export interface HandlerError { 37 + status: number; 38 + message?: string; 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA; 44 + params: QueryParams; 45 + input: HandlerInput; 46 + req: express.Request; 47 + res: express.Response; 48 + resetRouteRateLimits: () => Promise<void>; 49 + }; 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput;
+69
__generated__/types/social/grain/photo/applyAlts.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.photo.applyAlts"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + writes: Update[]; 17 + } 18 + 19 + export interface OutputSchema { 20 + /** True if the writes were successfully applied */ 21 + success?: boolean; 22 + } 23 + 24 + export interface HandlerInput { 25 + encoding: "application/json"; 26 + body: InputSchema; 27 + } 28 + 29 + export interface HandlerSuccess { 30 + encoding: "application/json"; 31 + body: OutputSchema; 32 + headers?: { [key: string]: string }; 33 + } 34 + 35 + export interface HandlerError { 36 + status: number; 37 + message?: string; 38 + } 39 + 40 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 41 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 42 + auth: HA; 43 + params: QueryParams; 44 + input: HandlerInput; 45 + req: express.Request; 46 + res: express.Response; 47 + resetRouteRateLimits: () => Promise<void>; 48 + }; 49 + export type Handler<HA extends HandlerAuth = never> = ( 50 + ctx: HandlerReqCtx<HA>, 51 + ) => Promise<HandlerOutput> | HandlerOutput; 52 + 53 + export interface Update { 54 + $type?: "social.grain.photo.applyAlts#update"; 55 + /** AT URI of the item to update */ 56 + photoUri: string; 57 + /** The alt text to apply to the photo */ 58 + alt: string; 59 + } 60 + 61 + const hashUpdate = "update"; 62 + 63 + export function isUpdate<V>(v: V) { 64 + return is$typed(v, id, hashUpdate); 65 + } 66 + 67 + export function validateUpdate<V>(v: V) { 68 + return validate<Update & V>(v, id, hashUpdate); 69 + }
+61
__generated__/types/social/grain/photo/createExif.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.photo.createExif"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + photo: string; 17 + dateTimeOriginal?: string; 18 + exposureTime?: number; 19 + fNumber?: number; 20 + flash?: string; 21 + focalLengthIn35mmFormat?: number; 22 + iSO?: number; 23 + lensMake?: string; 24 + lensModel?: string; 25 + make?: string; 26 + model?: string; 27 + } 28 + 29 + export interface OutputSchema { 30 + /** AT URI of the created gallery */ 31 + exifUri?: string; 32 + } 33 + 34 + export interface HandlerInput { 35 + encoding: "application/json"; 36 + body: InputSchema; 37 + } 38 + 39 + export interface HandlerSuccess { 40 + encoding: "application/json"; 41 + body: OutputSchema; 42 + headers?: { [key: string]: string }; 43 + } 44 + 45 + export interface HandlerError { 46 + status: number; 47 + message?: string; 48 + } 49 + 50 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 51 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 52 + auth: HA; 53 + params: QueryParams; 54 + input: HandlerInput; 55 + req: express.Request; 56 + res: express.Response; 57 + resetRouteRateLimits: () => Promise<void>; 58 + }; 59 + export type Handler<HA extends HandlerAuth = never> = ( 60 + ctx: HandlerReqCtx<HA>, 61 + ) => Promise<HandlerOutput> | HandlerOutput;
+52
__generated__/types/social/grain/photo/deletePhoto.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate; 11 + const id = "social.grain.photo.deletePhoto"; 12 + 13 + export interface QueryParams {} 14 + 15 + export interface InputSchema { 16 + /** AT URI of the photo to delete. */ 17 + uri: string; 18 + } 19 + 20 + export interface OutputSchema { 21 + /** Indicates if the photo was successfully deleted. */ 22 + success: boolean; 23 + } 24 + 25 + export interface HandlerInput { 26 + encoding: "application/json"; 27 + body: InputSchema; 28 + } 29 + 30 + export interface HandlerSuccess { 31 + encoding: "application/json"; 32 + body: OutputSchema; 33 + headers?: { [key: string]: string }; 34 + } 35 + 36 + export interface HandlerError { 37 + status: number; 38 + message?: string; 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA; 44 + params: QueryParams; 45 + input: HandlerInput; 46 + req: express.Request; 47 + res: express.Response; 48 + resetRouteRateLimits: () => Promise<void>; 49 + }; 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput;
+50
__generated__/types/social/grain/photo/uploadPhoto.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import stream from "node:stream"; 5 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 6 + import express from "npm:express"; 7 + import { validate as _validate } from "../../../../lexicons.ts"; 8 + import { is$typed as _is$typed } from "../../../../util.ts"; 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate; 12 + const id = "social.grain.photo.uploadPhoto"; 13 + 14 + export interface QueryParams {} 15 + 16 + export type InputSchema = string | Uint8Array | Blob; 17 + 18 + export interface OutputSchema { 19 + /** AT URI of the created photo */ 20 + photoUri?: string; 21 + } 22 + 23 + export interface HandlerInput { 24 + encoding: "*/*"; 25 + body: stream.Readable; 26 + } 27 + 28 + export interface HandlerSuccess { 29 + encoding: "application/json"; 30 + body: OutputSchema; 31 + headers?: { [key: string]: string }; 32 + } 33 + 34 + export interface HandlerError { 35 + status: number; 36 + message?: string; 37 + } 38 + 39 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 40 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 41 + auth: HA; 42 + params: QueryParams; 43 + input: HandlerInput; 44 + req: express.Request; 45 + res: express.Response; 46 + resetRouteRateLimits: () => Promise<void>; 47 + }; 48 + export type Handler<HA extends HandlerAuth = never> = ( 49 + ctx: HandlerReqCtx<HA>, 50 + ) => Promise<HandlerOutput> | HandlerOutput;
+2 -1
deno.json
··· 3 3 "$lexicon/": "./__generated__/", 4 4 "@atproto/api": "npm:@atproto/api@^0.15.16", 5 5 "@atproto/syntax": "npm:@atproto/syntax@^0.4.0", 6 - "@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.52", 6 + "@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.54", 7 7 "@std/http": "jsr:@std/http@^1.0.17", 8 8 "@std/path": "jsr:@std/path@^1.0.9", 9 9 "@tailwindcss/cli": "npm:@tailwindcss/cli@^4.1.4", ··· 11 11 "exifr": "npm:exifr@^7.1.3", 12 12 "htmx.org": "npm:htmx.org@^1.9.12", 13 13 "hyperscript.org": "npm:hyperscript.org@^0.9.14", 14 + "image-size": "npm:image-size@^2.0.2", 14 15 "popmotion": "npm:popmotion@^11.0.5", 15 16 "preact": "npm:preact@^10.26.5", 16 17 "sortablejs": "npm:sortablejs@^1.15.6",
+49 -1
deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 + "jsr:@bigmoves/atproto-oauth-client@0.2": "0.2.0", 5 + "jsr:@bigmoves/bff@0.3.0-beta.54": "0.3.0-beta.54", 4 6 "jsr:@deno/gfm@0.10": "0.10.0", 5 7 "jsr:@denosaurs/emoji@0.3": "0.3.1", 6 8 "jsr:@luca/esbuild-deno-loader@~0.11.1": "0.11.1", ··· 8 10 "jsr:@std/assert@^1.0.13": "1.0.13", 9 11 "jsr:@std/async@^1.0.12": "1.0.12", 10 12 "jsr:@std/bytes@^1.0.2": "1.0.6", 13 + "jsr:@std/cache@0.2": "0.2.0", 11 14 "jsr:@std/cli@^1.0.16": "1.0.20", 12 15 "jsr:@std/cli@^1.0.20": "1.0.20", 13 16 "jsr:@std/data-structures@^1.0.6": "1.0.7", ··· 64 67 "npm:he@^1.2.0": "1.2.0", 65 68 "npm:htmx.org@^1.9.12": "1.9.12", 66 69 "npm:hyperscript.org@~0.9.14": "0.9.14", 70 + "npm:image-size@^2.0.2": "2.0.2", 67 71 "npm:jose@5.9.6": "5.9.6", 68 72 "npm:jsonwebtoken@^9.0.2": "9.0.2", 69 73 "npm:katex@0.16": "0.16.22", ··· 86 90 "npm:typed-htmx@~0.3.1": "0.3.1" 87 91 }, 88 92 "jsr": { 93 + "@bigmoves/atproto-oauth-client@0.2.0": { 94 + "integrity": "5c3ca124dd52eff51dace83790779ebe48c4b41559b799e16c8750bd415f2124", 95 + "dependencies": [ 96 + "npm:@atproto-labs/handle-resolver-node", 97 + "npm:@atproto-labs/simple-store", 98 + "npm:@atproto/jwk", 99 + "npm:@atproto/oauth-client", 100 + "npm:@atproto/oauth-types", 101 + "npm:jose" 102 + ] 103 + }, 104 + "@bigmoves/bff@0.3.0-beta.54": { 105 + "integrity": "b3f04813eb9039c142dfc22c7d9bba6a897a2149fd430a45405c9f2df776843a", 106 + "dependencies": [ 107 + "jsr:@bigmoves/atproto-oauth-client", 108 + "jsr:@std/assert@^1.0.13", 109 + "jsr:@std/cache", 110 + "jsr:@std/fmt", 111 + "jsr:@std/http@^1.0.13", 112 + "jsr:@std/path@^1.0.8", 113 + "npm:@atproto/api@~0.15.7", 114 + "npm:@atproto/common", 115 + "npm:@atproto/identity", 116 + "npm:@atproto/lexicon@0.4.11", 117 + "npm:@atproto/lexicon@~0.4.11", 118 + "npm:@atproto/oauth-client", 119 + "npm:@atproto/syntax", 120 + "npm:@atproto/xrpc-server@0.7.19", 121 + "npm:clsx", 122 + "npm:jsonwebtoken", 123 + "npm:multiformats@^13.3.2", 124 + "npm:preact", 125 + "npm:preact-render-to-string", 126 + "npm:tailwind-merge" 127 + ] 128 + }, 89 129 "@deno/gfm@0.10.0": { 90 130 "integrity": "51708205e3559a4aeb6afb29d07c5bfafe7941f91bb360351ef6621de9a39527", 91 131 "dependencies": [ ··· 123 163 }, 124 164 "@std/bytes@1.0.6": { 125 165 "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" 166 + }, 167 + "@std/cache@0.2.0": { 168 + "integrity": "63a2ccd5a9e7c03e430f7d34dfcfd0d0cfc90731a1eaf8208f4c66e418fc3035" 126 169 }, 127 170 "@std/cli@1.0.20": { 128 171 "integrity": "a8c384a2c98cec6ec6a2055c273a916e2772485eb784af0db004c5ab8ba52333" ··· 1408 1451 "ieee754@1.2.1": { 1409 1452 "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" 1410 1453 }, 1454 + "image-size@2.0.2": { 1455 + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", 1456 + "bin": true 1457 + }, 1411 1458 "inherits@2.0.4": { 1412 1459 "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1413 1460 }, ··· 2087 2134 }, 2088 2135 "workspace": { 2089 2136 "dependencies": [ 2090 - "jsr:@bigmoves/bff@0.3.0-beta.52", 2137 + "jsr:@bigmoves/bff@0.3.0-beta.54", 2091 2138 "jsr:@std/http@^1.0.17", 2092 2139 "jsr:@std/path@^1.0.9", 2093 2140 "npm:@atproto/api@~0.15.16", ··· 2097 2144 "npm:exifr@^7.1.3", 2098 2145 "npm:htmx.org@^1.9.12", 2099 2146 "npm:hyperscript.org@~0.9.14", 2147 + "npm:image-size@^2.0.2", 2100 2148 "npm:popmotion@^11.0.5", 2101 2149 "npm:preact@^10.26.5", 2102 2150 "npm:sortablejs@^1.15.6",
+25
lexicons/social/grain/actor/updateAvatar.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.actor.updateAvatar", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Update an actor's avatar. Requires auth.", 8 + "input": { 9 + "encoding": "*/*" 10 + }, 11 + "output": { 12 + "encoding": "application/json", 13 + "schema": { 14 + "type": "object", 15 + "properties": { 16 + "success": { 17 + "type": "boolean", 18 + "description": "Indicates whether the avatar update was successful." 19 + } 20 + } 21 + } 22 + } 23 + } 24 + } 25 + }
+41
lexicons/social/grain/actor/updateProfile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.actor.updateProfile", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Update an actor's profile info. Requires auth.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "displayName": { 14 + "type": "string", 15 + "maxGraphemes": 64, 16 + "maxLength": 640 17 + }, 18 + "description": { 19 + "type": "string", 20 + "description": "Free-form profile description text.", 21 + "maxGraphemes": 256, 22 + "maxLength": 2560 23 + } 24 + } 25 + } 26 + }, 27 + "output": { 28 + "encoding": "application/json", 29 + "schema": { 30 + "type": "object", 31 + "properties": { 32 + "success": { 33 + "type": "boolean", 34 + "description": "Indicates whether the profile update was successful." 35 + } 36 + } 37 + } 38 + } 39 + } 40 + } 41 + }
+49
lexicons/social/grain/comment/createComment.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.comment.createComment", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create a comment. Requires auth.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["text", "subject"], 13 + "properties": { 14 + "text": { 15 + "type": "string", 16 + "maxLength": 3000, 17 + "maxGraphemes": 300 18 + }, 19 + "subject": { 20 + "type": "string", 21 + "format": "at-uri" 22 + }, 23 + "focus": { 24 + "type": "string", 25 + "format": "at-uri" 26 + }, 27 + "replyTo": { 28 + "type": "string", 29 + "format": "at-uri" 30 + } 31 + } 32 + } 33 + }, 34 + "output": { 35 + "encoding": "application/json", 36 + "schema": { 37 + "type": "object", 38 + "properties": { 39 + "commentUri": { 40 + "type": "string", 41 + "format": "at-uri", 42 + "description": "AT URI of the created comment" 43 + } 44 + } 45 + } 46 + } 47 + } 48 + } 49 + }
+36
lexicons/social/grain/comment/deleteComment.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.comment.deleteComment", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete a comment. Requires auth.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["uri"], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "AT URI of the comment to delete" 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "properties": { 27 + "success": { 28 + "type": "boolean", 29 + "description": "True if the comment was deleted" 30 + } 31 + } 32 + } 33 + } 34 + } 35 + } 36 + }
lexicons/social/grain/favorite.json lexicons/social/grain/favorite/favorite.json
+38
lexicons/social/grain/favorite/createFavorite.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.favorite.createFavorite", 4 + "defs": { 5 + "main": { 6 + "description": "Create a favorite for a given subject.", 7 + "type": "procedure", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["subject"], 13 + "properties": { 14 + "subject": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "URI of the subject to favorite." 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": ["favoriteUri"], 27 + "properties": { 28 + "favoriteUri": { 29 + "type": "string", 30 + "format": "at-uri", 31 + "description": "AT URI for the created favorite." 32 + } 33 + } 34 + } 35 + } 36 + } 37 + } 38 + }
+37
lexicons/social/grain/favorite/deleteFavorite.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.favorite.deleteFavorite", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete a favorite item by its ID.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["uri"], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "The AT URI of the favorite to delete." 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": ["success"], 27 + "properties": { 28 + "success": { 29 + "type": "boolean", 30 + "description": "Indicates if the favorite was successfully deleted." 31 + } 32 + } 33 + } 34 + } 35 + } 36 + } 37 + }
+53
lexicons/social/grain/gallery/applySort.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.gallery.applySort", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Apply sorting to photos in a gallery. Requires auth.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["writes"], 13 + "properties": { 14 + "writes": { 15 + "type": "array", 16 + "items": { 17 + "type": "ref", 18 + "ref": "#update" 19 + } 20 + } 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "object", 28 + "properties": { 29 + "success": { 30 + "type": "boolean", 31 + "description": "True if the writes were successfully applied" 32 + } 33 + } 34 + } 35 + } 36 + }, 37 + "update": { 38 + "type": "object", 39 + "required": ["itemUri", "position"], 40 + "properties": { 41 + "itemUri": { 42 + "type": "string", 43 + "format": "at-uri", 44 + "description": "AT URI of the item to update" 45 + }, 46 + "position": { 47 + "type": "integer", 48 + "description": "The position of the item in the gallery, used for ordering" 49 + } 50 + } 51 + } 52 + } 53 + }
+34
lexicons/social/grain/gallery/createGallery.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.gallery.createGallery", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create a new gallery", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["title"], 13 + "properties": { 14 + "title": { "type": "string", "maxLength": 100 }, 15 + "description": { "type": "string", "maxLength": 1000 } 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "properties": { 24 + "galleryUri": { 25 + "type": "string", 26 + "format": "at-uri", 27 + "description": "AT URI of the created gallery" 28 + } 29 + } 30 + } 31 + } 32 + } 33 + } 34 + }
+46
lexicons/social/grain/gallery/createItem.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.gallery.createItem", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create a new gallery item", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["galleryUri", "photoUri", "position"], 13 + "properties": { 14 + "galleryUri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "AT URI of the gallery to create the item in" 18 + }, 19 + "photoUri": { 20 + "type": "string", 21 + "format": "at-uri", 22 + "description": "AT URI of the photo to be added as an item" 23 + }, 24 + "position": { 25 + "type": "integer", 26 + "description": "Position of the item in the gallery, used for ordering" 27 + } 28 + } 29 + } 30 + }, 31 + "output": { 32 + "encoding": "application/json", 33 + "schema": { 34 + "type": "object", 35 + "properties": { 36 + "itemUri": { 37 + "type": "string", 38 + "format": "at-uri", 39 + "description": "AT URI of the created gallery item" 40 + } 41 + } 42 + } 43 + } 44 + } 45 + } 46 + }
+36
lexicons/social/grain/gallery/deleteGallery.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.gallery.deleteGallery", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete a gallery. Does not delete the items in the gallery, just the gallery itself.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["uri"], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "Unique identifier of the gallery to delete" 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "properties": { 27 + "success": { 28 + "type": "boolean", 29 + "description": "True if the gallery was deleted" 30 + } 31 + } 32 + } 33 + } 34 + } 35 + } 36 + }
+36
lexicons/social/grain/gallery/deleteItem.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.gallery.deleteItem", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete a gallery item", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["uri"], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "AT URI of the gallery to create the item in" 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "properties": { 27 + "success": { 28 + "type": "boolean", 29 + "description": "True if the gallery item was deleted" 30 + } 31 + } 32 + } 33 + } 34 + } 35 + } 36 + }
+38
lexicons/social/grain/gallery/updateGallery.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.gallery.updateGallery", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create a new gallery", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["galleryUri", "title"], 13 + "properties": { 14 + "galleryUri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "The AT-URI of the gallery to update" 18 + }, 19 + "title": { "type": "string" }, 20 + "description": { "type": "string" } 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "object", 28 + "properties": { 29 + "success": { 30 + "type": "boolean", 31 + "description": "True if the gallery was updated" 32 + } 33 + } 34 + } 35 + } 36 + } 37 + } 38 + }
+37
lexicons/social/grain/graph/createFollow.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.graph.createFollow", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create a follow relationship between actors.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["subject"], 13 + "properties": { 14 + "subject": { 15 + "type": "string", 16 + "format": "at-identifier", 17 + "description": "DID of the actor to follow." 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "properties": { 27 + "followUri": { 28 + "type": "string", 29 + "format": "at-uri", 30 + "description": "AT URI of the created follow record." 31 + } 32 + } 33 + } 34 + } 35 + } 36 + } 37 + }
+36
lexicons/social/grain/graph/deleteFollow.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.graph.deleteFollow", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete a follow relationship. Requires auth.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["uri"], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "AT URI of the follow record to delete" 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "properties": { 27 + "success": { 28 + "type": "boolean", 29 + "description": "True if the follow was deleted" 30 + } 31 + } 32 + } 33 + } 34 + } 35 + } 36 + }
+54
lexicons/social/grain/photo/applyAlts.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.photo.applyAlts", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Apply alt texts to photos in a gallery. Requires auth.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["writes"], 13 + "properties": { 14 + "writes": { 15 + "type": "array", 16 + "items": { 17 + "type": "ref", 18 + "ref": "#update" 19 + } 20 + } 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "object", 28 + "properties": { 29 + "success": { 30 + "type": "boolean", 31 + "description": "True if the writes were successfully applied" 32 + } 33 + } 34 + } 35 + } 36 + }, 37 + "update": { 38 + "type": "object", 39 + "required": ["photoUri", "alt"], 40 + "properties": { 41 + "photoUri": { 42 + "type": "string", 43 + "format": "at-uri", 44 + "description": "AT URI of the item to update" 45 + }, 46 + "alt": { 47 + "type": "string", 48 + "maxLength": 1000, 49 + "description": "The alt text to apply to the photo" 50 + } 51 + } 52 + } 53 + } 54 + }
+43
lexicons/social/grain/photo/createExif.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.photo.createExif", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create a new Exif record for a photo", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["photo"], 13 + "properties": { 14 + "photo": { "type": "string", "format": "at-uri" }, 15 + "dateTimeOriginal": { "type": "string", "format": "datetime" }, 16 + "exposureTime": { "type": "integer" }, 17 + "fNumber": { "type": "integer" }, 18 + "flash": { "type": "string" }, 19 + "focalLengthIn35mmFormat": { "type": "integer" }, 20 + "iSO": { "type": "integer" }, 21 + "lensMake": { "type": "string" }, 22 + "lensModel": { "type": "string" }, 23 + "make": { "type": "string" }, 24 + "model": { "type": "string" } 25 + } 26 + } 27 + }, 28 + "output": { 29 + "encoding": "application/json", 30 + "schema": { 31 + "type": "object", 32 + "properties": { 33 + "exifUri": { 34 + "type": "string", 35 + "format": "at-uri", 36 + "description": "AT URI of the created gallery" 37 + } 38 + } 39 + } 40 + } 41 + } 42 + } 43 + }
+37
lexicons/social/grain/photo/deletePhoto.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.photo.deletePhoto", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete a favorite photo by its unique at-uri.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["uri"], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "AT URI of the photo to delete." 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": ["success"], 27 + "properties": { 28 + "success": { 29 + "type": "boolean", 30 + "description": "Indicates if the photo was successfully deleted." 31 + } 32 + } 33 + } 34 + } 35 + } 36 + } 37 + }
+26
lexicons/social/grain/photo/uploadPhoto.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.photo.uploadPhoto", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Upload a photo. Requires auth.", 8 + "input": { 9 + "encoding": "*/*" 10 + }, 11 + "output": { 12 + "encoding": "application/json", 13 + "schema": { 14 + "type": "object", 15 + "properties": { 16 + "photoUri": { 17 + "type": "string", 18 + "format": "at-uri", 19 + "description": "AT URI of the created photo" 20 + } 21 + } 22 + } 23 + } 24 + } 25 + } 26 + }
+748 -43
src/api/mod.ts
··· 12 12 QueryParams as SearchActorsQueryParams, 13 13 } from "$lexicon/types/social/grain/actor/searchActors.ts"; 14 14 import { 15 + OutputSchema as UpdateAvatarOutputSchema, 16 + } from "$lexicon/types/social/grain/actor/updateAvatar.ts"; 17 + import { 18 + InputSchema as UpdateProfileInputSchema, 19 + OutputSchema as UpdateProfileOutputSchema, 20 + } from "$lexicon/types/social/grain/actor/updateProfile.ts"; 21 + import { 22 + InputSchema as CreateCommentInputSchema, 23 + OutputSchema as CreateCommentOutputSchema, 24 + } from "$lexicon/types/social/grain/comment/createComment.ts"; 25 + import { 26 + InputSchema as DeleteCommentInputSchema, 27 + OutputSchema as DeleteCommentOutputSchema, 28 + } from "$lexicon/types/social/grain/comment/deleteComment.ts"; 29 + import { 30 + InputSchema as CreateFavoriteInputSchema, 31 + OutputSchema as CreateFavoriteOutputSchema, 32 + } from "$lexicon/types/social/grain/favorite/createFavorite.ts"; 33 + import { 34 + InputSchema as DeleteFavoriteInputSchema, 35 + OutputSchema as DeleteFavoriteOutputSchema, 36 + } from "$lexicon/types/social/grain/favorite/deleteFavorite.ts"; 37 + import { 15 38 OutputSchema as GetTimelineOutputSchema, 16 39 QueryParams as GetTimelineQueryParams, 17 40 } from "$lexicon/types/social/grain/feed/getTimeline.ts"; 18 41 import { 42 + InputSchema as ApplySortInputSchema, 43 + OutputSchema as ApplySortOutputSchema, 44 + } from "$lexicon/types/social/grain/gallery/applySort.ts"; 45 + import { 46 + InputSchema as CreateGalleryInputSchema, 47 + OutputSchema as CreateGalleryOutputSchema, 48 + } from "$lexicon/types/social/grain/gallery/createGallery.ts"; 49 + import { 50 + InputSchema as CreateGalleryItemInputSchema, 51 + OutputSchema as CreateGalleryItemOutputSchema, 52 + } from "$lexicon/types/social/grain/gallery/createItem.ts"; 53 + import { 54 + InputSchema as DeleteGalleryInputSchema, 55 + OutputSchema as DeleteGalleryOutputSchema, 56 + } from "$lexicon/types/social/grain/gallery/deleteGallery.ts"; 57 + import { 58 + InputSchema as DeleteGalleryItemInputSchema, 59 + OutputSchema as DeleteGalleryItemOutputSchema, 60 + } from "$lexicon/types/social/grain/gallery/deleteItem.ts"; 61 + import { 19 62 OutputSchema as GetActorGalleriesOutputSchema, 20 63 QueryParams as GetActorGalleriesQueryParams, 21 64 } from "$lexicon/types/social/grain/gallery/getActorGalleries.ts"; ··· 28 71 QueryParams as GetGalleryThreadQueryParams, 29 72 } from "$lexicon/types/social/grain/gallery/getGalleryThread.ts"; 30 73 import { 74 + InputSchema as UpdateGalleryInputSchema, 75 + OutputSchema as UpdateGalleryOutputSchema, 76 + } from "$lexicon/types/social/grain/gallery/updateGallery.ts"; 77 + import { 78 + InputSchema as CreateFollowInputSchema, 79 + OutputSchema as CreateFollowOutputSchema, 80 + } from "$lexicon/types/social/grain/graph/createFollow.ts"; 81 + import { 82 + InputSchema as DeleteFollowInputSchema, 83 + OutputSchema as DeleteFollowOutputSchema, 84 + } from "$lexicon/types/social/grain/graph/deleteFollow.ts"; 85 + import { 31 86 OutputSchema as GetFollowersOutputSchema, 32 87 QueryParams as GetFollowersQueryParams, 33 88 } from "$lexicon/types/social/grain/graph/getFollowers.ts"; ··· 38 93 import { 39 94 OutputSchema as GetNotificationsOutputSchema, 40 95 } from "$lexicon/types/social/grain/notification/getNotifications.ts"; 96 + import { 97 + InputSchema as ApplyAltsInputSchema, 98 + OutputSchema as ApplyAltsOutputSchema, 99 + } from "$lexicon/types/social/grain/photo/applyAlts.ts"; 100 + import { 101 + InputSchema as DeletePhotoInputSchema, 102 + OutputSchema as DeletePhotoOutputSchema, 103 + } from "$lexicon/types/social/grain/photo/deletePhoto.ts"; 41 104 import { 42 105 OutputSchema as GetActorPhotosOutputSchema, 43 106 QueryParams as GetActorPhotosQueryParams, 44 107 } from "$lexicon/types/social/grain/photo/getActorPhotos.ts"; 45 - 108 + import { 109 + OutputSchema as UploadPhotoOutputSchema, 110 + } from "$lexicon/types/social/grain/photo/uploadPhoto.ts"; 46 111 import { AtUri } from "@atproto/syntax"; 47 112 import { BffMiddleware, route } from "@bigmoves/bff"; 113 + import { imageSize } from "image-size"; 114 + import { Buffer } from "node:buffer"; 48 115 import { 49 116 getActorGalleries, 50 117 getActorGalleryFavs, ··· 52 119 getActorProfile, 53 120 getActorProfileDetailed, 54 121 searchActors, 122 + updateActorProfile, 55 123 } from "../lib/actor.ts"; 56 - import { BadRequestError } from "../lib/errors.ts"; 124 + import { XRPCError } from "../lib/errors.ts"; 125 + import { createFavorite } from "../lib/favs.ts"; 126 + import { 127 + applySort, 128 + createGallery, 129 + createGalleryItem, 130 + deleteGallery, 131 + getGalleriesByHashtag, 132 + getGallery, 133 + updateGallery, 134 + } from "../lib/gallery.ts"; 57 135 import { 136 + createFollow, 58 137 getFollowersWithProfiles, 59 138 getFollowingWithProfiles, 60 - } from "../lib/follow.ts"; 61 - import { getGalleriesByHashtag, getGallery } from "../lib/gallery.ts"; 139 + } from "../lib/graph.ts"; 62 140 import { getNotificationsDetailed } from "../lib/notifications.ts"; 141 + import { applyAlts, createExif, createPhoto } from "../lib/photo.ts"; 63 142 import { getTimeline } from "../lib/timeline.ts"; 64 - import { getGalleryComments } from "../modules/comments.tsx"; 143 + import { createComment, getGalleryComments } from "../modules/comments.tsx"; 65 144 66 145 export const middlewares: BffMiddleware[] = [ 146 + route( 147 + "/xrpc/social.grain.gallery.createGallery", 148 + ["POST"], 149 + async (req, _params, ctx) => { 150 + ctx.requireAuth(); 151 + const { title, description } = await parseCreateGalleryInputs(req); 152 + 153 + try { 154 + const galleryUri = await createGallery(ctx, { title, description }); 155 + return ctx.json({ galleryUri } satisfies CreateGalleryOutputSchema); 156 + } catch (error) { 157 + console.error("Error creating gallery:", error); 158 + throw new XRPCError("InternalServerError", "Failed to create gallery"); 159 + } 160 + }, 161 + ), 162 + route( 163 + "/xrpc/social.grain.gallery.updateGallery", 164 + ["POST"], 165 + async (req, _params, ctx) => { 166 + ctx.requireAuth(); 167 + const { galleryUri, title, description } = await parseUpdateGalleryInputs( 168 + req, 169 + ); 170 + const success = await updateGallery(ctx, galleryUri, { 171 + title, 172 + description, 173 + }); 174 + return ctx.json( 175 + { success } satisfies UpdateGalleryOutputSchema, 176 + ); 177 + }, 178 + ), 179 + route( 180 + "/xrpc/social.grain.gallery.deleteGallery", 181 + ["POST"], 182 + async (req, _params, ctx) => { 183 + ctx.requireAuth(); 184 + const { uri } = await parseDeleteGalleryInputs( 185 + req, 186 + ); 187 + const success = await deleteGallery(uri, ctx); 188 + return ctx.json({ success } satisfies DeleteGalleryOutputSchema); 189 + }, 190 + ), 191 + route( 192 + "/xrpc/social.grain.gallery.createItem", 193 + ["POST"], 194 + async (req, _params, ctx) => { 195 + ctx.requireAuth(); 196 + const { galleryUri, photoUri } = await parseCreateGalleryItemInputs(req); 197 + const createdItemUri = await createGalleryItem( 198 + ctx, 199 + galleryUri, 200 + photoUri, 201 + ); 202 + if (!createdItemUri) { 203 + return ctx.json( 204 + { message: "Failed to create gallery item" }, 205 + 400, 206 + ); 207 + } 208 + return ctx.json( 209 + { itemUri: createdItemUri } satisfies CreateGalleryItemOutputSchema, 210 + ); 211 + }, 212 + ), 213 + route( 214 + "/xrpc/social.grain.gallery.deleteItem", 215 + ["POST"], 216 + async (req, _params, ctx) => { 217 + ctx.requireAuth(); 218 + const { uri } = await parseDeleteGalleryItemInputs(req); 219 + try { 220 + await ctx.deleteRecord(uri); 221 + } catch (error) { 222 + console.error("Error deleting gallery item:", error); 223 + return ctx.json( 224 + { success: false } satisfies DeleteGalleryItemOutputSchema, 225 + ); 226 + } 227 + return ctx.json( 228 + { success: true } satisfies DeleteGalleryItemOutputSchema, 229 + ); 230 + }, 231 + ), 232 + route( 233 + "/xrpc/social.grain.photo.uploadPhoto", 234 + ["POST"], 235 + async (req, _params, ctx) => { 236 + ctx.requireAuth(); 237 + if (!ctx.agent) { 238 + return ctx.json( 239 + { message: "Unauthorized" }, 240 + 401, 241 + ); 242 + } 243 + 244 + const bytes = await req.arrayBuffer(); 245 + if (!bytes || bytes.byteLength === 0) { 246 + throw new XRPCError("InvalidRequest", "Missing blob"); 247 + } 248 + const MAX_SIZE = 1024 * 1024; // 1MB 249 + if (bytes.byteLength > MAX_SIZE) { 250 + throw new XRPCError( 251 + "PayloadTooLarge", 252 + "request entity too large", 253 + ); 254 + } 255 + const { width, height } = imageSize(Buffer.from(bytes)); 256 + const res = await ctx.agent.uploadBlob(new Uint8Array(bytes)); 257 + if (!res.success) { 258 + return ctx.json( 259 + { message: "Failed to upload photo" }, 260 + 500, 261 + ); 262 + } 263 + const blobRef = res.data.blob; 264 + const photoUri = await createPhoto( 265 + { 266 + photo: blobRef, 267 + alt: "", // @TODO: make this optional 268 + aspectRatio: { 269 + width, 270 + height, 271 + }, 272 + }, 273 + ctx, 274 + ); 275 + return ctx.json({ photoUri } as UploadPhotoOutputSchema); 276 + }, 277 + ), 278 + route( 279 + "/xrpc/social.grain.photo.createExif", 280 + ["POST"], 281 + async (req, _params, ctx) => { 282 + ctx.requireAuth(); 283 + const exifData = await parseExifInputs(req); 284 + const exifUri = await createExif( 285 + exifData, 286 + ctx, 287 + ); 288 + if (!exifUri) { 289 + return ctx.json( 290 + { message: "Failed to create EXIF data" }, 291 + 500, 292 + ); 293 + } 294 + return ctx.json({ exifUri }); 295 + }, 296 + ), 297 + route( 298 + "/xrpc/social.grain.photo.deletePhoto", 299 + ["POST"], 300 + async (req, _params, ctx) => { 301 + ctx.requireAuth(); 302 + const { uri } = await parseDeletePhotoInputs(req); 303 + try { 304 + await ctx.deleteRecord(uri); 305 + return ctx.json({ success: true } satisfies DeletePhotoOutputSchema); 306 + } catch (error) { 307 + console.error("Error deleting photo:", error); 308 + return ctx.json({ success: false } satisfies DeletePhotoOutputSchema); 309 + } 310 + }, 311 + ), 312 + route( 313 + "/xrpc/social.grain.graph.createFollow", 314 + ["POST"], 315 + async (req, _params, ctx) => { 316 + const { did } = ctx.requireAuth(); 317 + const { subject } = await parseCreateFollowInputs(req); 318 + if (!subject) { 319 + throw new XRPCError("InvalidRequest", "Missing subject input"); 320 + } 321 + try { 322 + const followUri = await createFollow(did, subject, ctx); 323 + return ctx.json({ followUri } satisfies CreateFollowOutputSchema); 324 + } catch (error) { 325 + console.error("Error creating follow:", error); 326 + throw new XRPCError("InternalServerError", "Failed to create follow"); 327 + } 328 + }, 329 + ), 330 + route( 331 + "/xrpc/social.grain.graph.deleteFollow", 332 + ["POST"], 333 + async (req, _params, ctx) => { 334 + ctx.requireAuth(); 335 + const { uri } = await parseDeleteFollowInputs(req); 336 + try { 337 + await ctx.deleteRecord(uri); 338 + return ctx.json({ success: true } satisfies DeleteFollowOutputSchema); 339 + } catch (error) { 340 + console.error("Error deleting follow:", error); 341 + return ctx.json({ success: false } satisfies DeleteFollowOutputSchema); 342 + } 343 + }, 344 + ), 345 + route( 346 + "/xrpc/social.grain.favorite.createFavorite", 347 + ["POST"], 348 + async (req, _params, ctx) => { 349 + ctx.requireAuth(); 350 + const { subject } = await parseCreateFavoriteInputs(req); 351 + try { 352 + const favoriteUri = await createFavorite(subject, ctx); 353 + return ctx.json({ favoriteUri } as CreateFavoriteOutputSchema); 354 + } catch (error) { 355 + console.error("Error creating favorite:", error); 356 + throw new XRPCError("InternalServerError", "Failed to create favorite"); 357 + } 358 + }, 359 + ), 360 + route( 361 + "/xrpc/social.grain.favorite.deleteFavorite", 362 + ["POST"], 363 + async (req, _params, ctx) => { 364 + ctx.requireAuth(); 365 + const { uri } = await parseDeleteFavoriteInputs(req); 366 + try { 367 + await ctx.deleteRecord(uri); 368 + return ctx.json({ success: true }); 369 + } catch (error) { 370 + console.error("Error deleting favorite:", error); 371 + return ctx.json({ success: false } as DeleteFavoriteOutputSchema); 372 + } 373 + }, 374 + ), 375 + route( 376 + "/xrpc/social.grain.comment.createComment", 377 + ["POST"], 378 + async (req, _params, ctx) => { 379 + ctx.requireAuth(); 380 + const { text, subject, focus, replyTo } = await parseCreateCommentInputs( 381 + req, 382 + ); 383 + try { 384 + const commentUri = await createComment( 385 + { 386 + text, 387 + subject, 388 + focus, 389 + replyTo, 390 + }, 391 + ctx, 392 + ); 393 + return ctx.json({ commentUri } satisfies CreateCommentOutputSchema); 394 + } catch (error) { 395 + console.error("Error creating comment:", error); 396 + throw new XRPCError("InternalServerError", "Failed to create comment"); 397 + } 398 + }, 399 + ), 400 + route( 401 + "/xrpc/social.grain.comment.deleteComment", 402 + ["POST"], 403 + async (req, _params, ctx) => { 404 + ctx.requireAuth(); 405 + const { uri } = await parseDeleteCommentInputs(req); 406 + try { 407 + await ctx.deleteRecord(uri); 408 + return ctx.json({ success: true } satisfies DeleteCommentOutputSchema); 409 + } catch (error) { 410 + console.error("Error deleting comment:", error); 411 + return ctx.json({ success: false } satisfies DeleteCommentOutputSchema); 412 + } 413 + }, 414 + ), 415 + route( 416 + "/xrpc/social.grain.actor.updateProfile", 417 + ["POST"], 418 + async (req, _params, ctx) => { 419 + const { did } = ctx.requireAuth(); 420 + const { displayName, description } = await parseUpdateProfileInputs(req); 421 + try { 422 + await updateActorProfile(did, ctx, { displayName, description }); 423 + } catch (error) { 424 + console.error("Error updating profile:", error); 425 + ctx.json({ success: false } satisfies UpdateProfileOutputSchema); 426 + } 427 + return ctx.json({ success: true } satisfies UpdateProfileOutputSchema); 428 + }, 429 + ), 430 + route( 431 + "/xrpc/social.grain.actor.updateAvatar", 432 + ["POST"], 433 + async (req, _params, ctx) => { 434 + const { did } = ctx.requireAuth(); 435 + const bytes = await req.arrayBuffer(); 436 + if (!bytes || bytes.byteLength === 0) { 437 + throw new XRPCError("InvalidRequest", "Missing avatar blob"); 438 + } 439 + const MAX_SIZE = 1024 * 1024; // 1MB 440 + if (bytes.byteLength > MAX_SIZE) { 441 + throw new XRPCError( 442 + "PayloadTooLarge", 443 + "request entity too large", 444 + ); 445 + } 446 + if (!ctx.agent) { 447 + throw new XRPCError("AuthenticationRequired"); 448 + } 449 + const res = await ctx.agent.uploadBlob(new Uint8Array(bytes)); 450 + if (!res.success) { 451 + throw new XRPCError("InternalServerError", "Failed to upload avatar"); 452 + } 453 + const avatarBlob = res.data.blob; 454 + try { 455 + await updateActorProfile(did, ctx, { avatar: avatarBlob }); 456 + } catch (error) { 457 + console.error("Error updating profile:", error); 458 + throw new XRPCError("InternalServerError", "Failed to update profile"); 459 + } 460 + return ctx.json({ success: true } satisfies UpdateAvatarOutputSchema); 461 + }, 462 + ), 463 + route( 464 + "/xrpc/social.grain.photo.applyAlts", 465 + ["POST"], 466 + async (req, _params, ctx) => { 467 + ctx.requireAuth(); 468 + const { writes } = await parseApplyAltsInputs(req); 469 + const success = await applyAlts(writes, ctx); 470 + return ctx.json({ success } satisfies ApplyAltsOutputSchema); 471 + }, 472 + ), 473 + route( 474 + "/xrpc/social.grain.gallery.applySort", 475 + ["POST"], 476 + async (req, _params, ctx) => { 477 + ctx.requireAuth(); 478 + const { writes } = await parseApplySortInputs(req); 479 + const success = await applySort(writes, ctx); 480 + return ctx.json({ success } satisfies ApplySortOutputSchema); 481 + }, 482 + ), 67 483 route("/xrpc/social.grain.actor.getProfile", (req, _params, ctx) => { 68 484 const url = new URL(req.url); 69 485 const { actor } = getProfileQueryParams(url); 70 486 const profile = getActorProfileDetailed(actor, ctx); 71 - return ctx.json(profile as GetProfileOutputSchema); 487 + if (!profile) { 488 + throw new XRPCError("NotFound", "Profile not found"); 489 + } 490 + return ctx.json(profile satisfies GetProfileOutputSchema); 72 491 }), 73 492 route("/xrpc/social.grain.gallery.getActorGalleries", (req, _params, ctx) => { 74 493 const url = new URL(req.url); 75 494 const { actor } = getActorGalleriesQueryParams(url); 76 495 const galleries = getActorGalleries(actor, ctx); 77 - return ctx.json({ items: galleries } as GetActorGalleriesOutputSchema); 496 + return ctx.json( 497 + { items: galleries } satisfies GetActorGalleriesOutputSchema, 498 + ); 78 499 }), 79 500 route("/xrpc/social.grain.actor.getActorFavs", (req, _params, ctx) => { 80 501 const url = new URL(req.url); 81 502 const { actor } = getActorFavsQueryParams(url); 82 503 const galleries = getActorGalleryFavs(actor, ctx); 83 - return ctx.json({ items: galleries } as GetActorFavsOutputSchema); 504 + return ctx.json({ items: galleries } satisfies GetActorFavsOutputSchema); 84 505 }), 85 506 route("/xrpc/social.grain.photo.getActorPhotos", (req, _params, ctx) => { 86 507 const url = new URL(req.url); 87 508 const { actor } = getActorPhotosQueryParams(url); 88 509 const photos = getActorPhotos(actor, ctx); 89 - return ctx.json({ items: photos } as GetActorPhotosOutputSchema); 510 + return ctx.json({ items: photos } satisfies GetActorPhotosOutputSchema); 90 511 }), 91 512 route("/xrpc/social.grain.gallery.getGallery", (req, _params, ctx) => { 92 513 const url = new URL(req.url); ··· 96 517 const rkey = atUri.rkey; 97 518 const gallery = getGallery(did, rkey, ctx); 98 519 if (!gallery) { 99 - return ctx.json({ message: "Gallery not found" }, 404); 520 + throw new XRPCError("NotFound", "Gallery not found"); 100 521 } 101 - return ctx.json(gallery as GetGalleryOutputSchema); 522 + return ctx.json(gallery satisfies GetGalleryOutputSchema); 102 523 }), 103 524 route("/xrpc/social.grain.gallery.getGalleryThread", (req, _params, ctx) => { 104 525 const url = new URL(req.url); ··· 108 529 const rkey = atUri.rkey; 109 530 const gallery = getGallery(did, rkey, ctx); 110 531 if (!gallery) { 111 - return ctx.json({ message: "Gallery not found" }, 404); 532 + throw new XRPCError("NotFound", "Gallery not found"); 112 533 } 113 534 const comments = getGalleryComments(uri, ctx); 114 - return ctx.json({ gallery, comments } as GetGalleryThreadOutputSchema); 535 + return ctx.json( 536 + { gallery, comments } satisfies GetGalleryThreadOutputSchema, 537 + ); 115 538 }), 116 539 route("/xrpc/social.grain.feed.getTimeline", async (req, _params, ctx) => { 117 540 const url = new URL(req.url); ··· 123 546 const galleries = getGalleriesByHashtag(tag, ctx); 124 547 125 548 return ctx.json( 126 - { feed: galleries } as GetTimelineOutputSchema, 549 + { feed: galleries } satisfies GetTimelineOutputSchema, 127 550 ); 128 551 } 129 552 ··· 133 556 "grain", 134 557 ); 135 558 return ctx.json( 136 - { feed: items.map((i) => i.gallery) } as GetTimelineOutputSchema, 559 + { feed: items.map((i) => i.gallery) } satisfies GetTimelineOutputSchema, 137 560 ); 138 561 }), 139 562 route( ··· 145 568 ctx, 146 569 ); 147 570 return ctx.json( 148 - { notifications } as GetNotificationsOutputSchema, 571 + { notifications } satisfies GetNotificationsOutputSchema, 149 572 ); 150 573 }, 151 574 ), ··· 164 587 ); 165 588 } 166 589 return ctx.json( 167 - { actors: results } as SearchActorsOutputSchema, 590 + { actors: results } satisfies SearchActorsOutputSchema, 168 591 ); 169 592 }, 170 593 ), ··· 172 595 const url = new URL(req.url); 173 596 const { actor } = getFollowsQueryParams(url); 174 597 const subject = getActorProfile(actor, ctx); 598 + if (!subject) { 599 + throw new XRPCError("NotFound", "Actor not found"); 600 + } 175 601 const follows = getFollowingWithProfiles(actor, ctx); 176 - return ctx.json({ 177 - subject, 178 - follows, 179 - } as GetFollowsOutputSchema); 602 + return ctx.json( 603 + { 604 + subject, 605 + follows, 606 + } satisfies GetFollowsOutputSchema, 607 + ); 180 608 }), 181 609 route("/xrpc/social.grain.graph.getFollowers", (req, _params, ctx) => { 182 610 const url = new URL(req.url); 183 611 const { actor } = getFollowersQueryParams(url); 184 612 const subject = getActorProfile(actor, ctx); 613 + if (!subject) { 614 + throw new XRPCError("NotFound", "Subject not found"); 615 + } 185 616 const followers = getFollowersWithProfiles(actor, ctx); 186 - return ctx.json({ 187 - subject, 188 - followers, 189 - } as GetFollowersOutputSchema); 617 + return ctx.json( 618 + { 619 + subject, 620 + followers, 621 + } satisfies GetFollowersOutputSchema, 622 + ); 190 623 }), 191 624 route( 192 625 "/xrpc/social.grain.notification.updateSeen", ··· 194 627 async (req, _params, ctx) => { 195 628 ctx.requireAuth(); 196 629 const json = await req.json(); 197 - const seenAt = json.seenAt as string ?? undefined; 630 + const seenAt = json.seenAt satisfies string ?? undefined; 198 631 if (!seenAt) { 199 - throw new BadRequestError("Missing seenAt input"); 632 + throw new XRPCError("InvalidRequest", "Missing seenAt input"); 200 633 } 201 634 ctx.updateSeen(seenAt); 202 635 return ctx.json(null); ··· 206 639 207 640 function getProfileQueryParams(url: URL): GetProfileQueryParams { 208 641 const actor = url.searchParams.get("actor"); 209 - if (!actor) throw new BadRequestError("Missing actor parameter"); 642 + if (!actor) throw new XRPCError("InvalidRequest", "Missing actor parameter"); 210 643 return { actor }; 211 644 } 212 645 213 646 function getActorGalleriesQueryParams(url: URL): GetActorGalleriesQueryParams { 214 647 const actor = url.searchParams.get("actor"); 215 - if (!actor) throw new BadRequestError("Missing actor parameter"); 648 + if (!actor) throw new XRPCError("InvalidRequest", "Missing actor parameter"); 216 649 const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 217 650 if (isNaN(limit) || limit <= 0) { 218 - throw new BadRequestError("Invalid limit parameter"); 651 + throw new XRPCError("InvalidRequest", "Invalid limit parameter"); 219 652 } 220 653 const cursor = url.searchParams.get("cursor") ?? undefined; 221 654 return { actor, limit, cursor }; ··· 223 656 224 657 function getActorFavsQueryParams(url: URL): GetActorFavsQueryParams { 225 658 const actor = url.searchParams.get("actor"); 226 - if (!actor) throw new BadRequestError("Missing actor parameter"); 659 + if (!actor) throw new XRPCError("InvalidRequest", "Missing actor parameter"); 227 660 const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 228 661 if (isNaN(limit) || limit <= 0) { 229 - throw new BadRequestError("Invalid limit parameter"); 662 + throw new XRPCError("InvalidRequest", "Invalid limit parameter"); 230 663 } 231 664 const cursor = url.searchParams.get("cursor") ?? undefined; 232 665 return { actor, limit, cursor }; ··· 234 667 235 668 function getActorPhotosQueryParams(url: URL): GetActorPhotosQueryParams { 236 669 const actor = url.searchParams.get("actor"); 237 - if (!actor) throw new BadRequestError("Missing actor parameter"); 670 + if (!actor) throw new XRPCError("InvalidRequest", "Missing actor parameter"); 238 671 const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 239 672 if (isNaN(limit) || limit <= 0) { 240 - throw new BadRequestError("Invalid limit parameter"); 673 + throw new XRPCError("InvalidRequest", "Invalid limit parameter"); 241 674 } 242 675 const cursor = url.searchParams.get("cursor") ?? undefined; 243 676 return { actor, limit, cursor }; ··· 245 678 246 679 function getGalleryQueryParams(url: URL): GetGalleryQueryParams { 247 680 const uri = url.searchParams.get("uri"); 248 - if (!uri) throw new BadRequestError("Missing uri parameter"); 681 + if (!uri) throw new XRPCError("InvalidRequest", "Missing uri parameter"); 249 682 return { uri }; 250 683 } 251 684 252 685 function getGalleryThreadQueryParams(url: URL): GetGalleryThreadQueryParams { 253 686 const uri = url.searchParams.get("uri"); 254 - if (!uri) throw new BadRequestError("Missing uri parameter"); 687 + if (!uri) throw new XRPCError("InvalidRequest", "Missing uri parameter"); 255 688 return { uri }; 256 689 } 257 690 258 691 function searchActorsQueryParams(url: URL): SearchActorsQueryParams { 259 692 const q = url.searchParams.get("q"); 260 - if (!q) throw new BadRequestError("Missing q parameter"); 693 + if (!q) throw new XRPCError("InvalidRequest", "Missing q parameter"); 261 694 const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 262 695 if (isNaN(limit) || limit <= 0) { 263 - throw new BadRequestError("Invalid limit parameter"); 696 + throw new XRPCError("InvalidRequest", "Invalid limit parameter"); 264 697 } 265 698 const cursor = url.searchParams.get("cursor") ?? undefined; 266 699 return { q, limit, cursor }; ··· 268 701 269 702 function getFollowsQueryParams(url: URL): GetFollowsQueryParams { 270 703 const actor = url.searchParams.get("actor"); 271 - if (!actor) throw new BadRequestError("Missing actor parameter"); 704 + if (!actor) throw new XRPCError("InvalidRequest", "Missing actor parameter"); 272 705 const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 273 706 if (isNaN(limit) || limit <= 0) { 274 - throw new BadRequestError("Invalid limit parameter"); 707 + throw new XRPCError("InvalidRequest", "Invalid limit parameter"); 275 708 } 276 709 const cursor = url.searchParams.get("cursor") ?? undefined; 277 710 return { actor, limit, cursor }; ··· 279 712 280 713 function getFollowersQueryParams(url: URL): GetFollowersQueryParams { 281 714 const actor = url.searchParams.get("actor"); 282 - if (!actor) throw new BadRequestError("Missing actor parameter"); 715 + if (!actor) throw new XRPCError("InvalidRequest", "Missing actor parameter"); 283 716 const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 284 717 if (isNaN(limit) || limit <= 0) { 285 - throw new BadRequestError("Invalid limit parameter"); 718 + throw new XRPCError("InvalidRequest", "Invalid limit parameter"); 286 719 } 287 720 const cursor = url.searchParams.get("cursor") ?? undefined; 288 721 return { actor, limit, cursor }; ··· 292 725 const algorithm = url.searchParams.get("algorithm") ?? undefined; 293 726 const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 294 727 if (isNaN(limit) || limit <= 0) { 295 - throw new BadRequestError("Invalid limit parameter"); 728 + throw new XRPCError("InvalidRequest", "Invalid limit parameter"); 296 729 } 297 730 const cursor = url.searchParams.get("cursor") ?? undefined; 298 731 return { algorithm, limit, cursor }; 299 732 } 733 + 734 + async function parseCreateGalleryInputs( 735 + req: Request, 736 + ): Promise<CreateGalleryInputSchema> { 737 + const body = await req.json(); 738 + const title = typeof body.title === "string" ? body.title : undefined; 739 + if (!title) { 740 + throw new XRPCError("InvalidRequest", "Missing title input"); 741 + } 742 + const description = typeof body.description === "string" 743 + ? body.description 744 + : undefined; 745 + return { title, description }; 746 + } 747 + 748 + async function parseUpdateGalleryInputs( 749 + req: Request, 750 + ): Promise<UpdateGalleryInputSchema> { 751 + const body = await req.json(); 752 + const title = typeof body.title === "string" ? body.title : undefined; 753 + if (!title) { 754 + throw new XRPCError("InvalidRequest", "Missing title input"); 755 + } 756 + const description = typeof body.description === "string" 757 + ? body.description 758 + : undefined; 759 + const galleryUri = typeof body.galleryUri === "string" 760 + ? body.galleryUri 761 + : undefined; 762 + if (!galleryUri) { 763 + throw new XRPCError("InvalidRequest", "Missing galleryUri input"); 764 + } 765 + return { title, description, galleryUri }; 766 + } 767 + 768 + async function parseDeleteGalleryInputs( 769 + req: Request, 770 + ): Promise<DeleteGalleryInputSchema> { 771 + const body = await req.json(); 772 + const uri = typeof body.uri === "string" ? body.uri : undefined; 773 + if (!uri) { 774 + throw new XRPCError("InvalidRequest", "Missing uri input"); 775 + } 776 + return { uri }; 777 + } 778 + 779 + async function parseCreateGalleryItemInputs( 780 + req: Request, 781 + ): Promise<CreateGalleryItemInputSchema> { 782 + const body = await req.json(); 783 + const galleryUri = typeof body.galleryUri === "string" 784 + ? body.galleryUri 785 + : undefined; 786 + if (!galleryUri) { 787 + throw new XRPCError("InvalidRequest", "Missing galleryUri input"); 788 + } 789 + const photoUri = typeof body.photoUri === "string" 790 + ? body.photoUri 791 + : undefined; 792 + if (!photoUri) { 793 + throw new XRPCError("InvalidRequest", "Missing photoUri input"); 794 + } 795 + const position = typeof body.position === "number" 796 + ? body.position 797 + : undefined; 798 + if (position === undefined) { 799 + throw new XRPCError("InvalidRequest", "Missing position input"); 800 + } 801 + return { galleryUri, photoUri, position }; 802 + } 803 + 804 + async function parseDeleteGalleryItemInputs( 805 + req: Request, 806 + ): Promise<DeleteGalleryItemInputSchema> { 807 + const body = await req.json(); 808 + const uri = typeof body.uri === "string" ? body.uri : undefined; 809 + if (!uri) { 810 + throw new XRPCError("InvalidRequest", "Missing uri input"); 811 + } 812 + return { uri }; 813 + } 814 + 815 + async function parseDeletePhotoInputs( 816 + req: Request, 817 + ): Promise<DeletePhotoInputSchema> { 818 + const body = await req.json(); 819 + const uri = typeof body.uri === "string" ? body.uri : undefined; 820 + if (!uri) { 821 + throw new XRPCError("InvalidRequest", "Missing uri input"); 822 + } 823 + return { uri }; 824 + } 825 + 826 + async function parseCreateFollowInputs( 827 + req: Request, 828 + ): Promise<CreateFollowInputSchema> { 829 + const body = await req.json(); 830 + const subject = typeof body.subject === "string" ? body.subject : undefined; 831 + if (!subject) { 832 + throw new XRPCError("InvalidRequest", "Missing subject input"); 833 + } 834 + return { subject }; 835 + } 836 + 837 + async function parseDeleteFollowInputs( 838 + req: Request, 839 + ): Promise<DeleteFollowInputSchema> { 840 + const body = await req.json(); 841 + const uri = typeof body.uri === "string" ? body.uri : undefined; 842 + if (!uri) { 843 + throw new XRPCError("InvalidRequest", "Missing uri input"); 844 + } 845 + return { uri }; 846 + } 847 + 848 + async function parseCreateFavoriteInputs( 849 + req: Request, 850 + ): Promise<CreateFavoriteInputSchema> { 851 + const body = await req.json(); 852 + const subject = typeof body.subject === "string" ? body.subject : undefined; 853 + if (!subject) { 854 + throw new XRPCError("InvalidRequest", "Missing subject input"); 855 + } 856 + return { subject }; 857 + } 858 + 859 + async function parseDeleteFavoriteInputs( 860 + req: Request, 861 + ): Promise<DeleteFavoriteInputSchema> { 862 + const body = await req.json(); 863 + const uri = typeof body.uri === "string" ? body.uri : undefined; 864 + if (!uri) { 865 + throw new XRPCError("InvalidRequest", "Missing uri input"); 866 + } 867 + return { uri }; 868 + } 869 + 870 + async function parseCreateCommentInputs( 871 + req: Request, 872 + ): Promise<CreateCommentInputSchema> { 873 + const body = await req.json(); 874 + const text = typeof body.text === "string" ? body.text : undefined; 875 + if (!text) { 876 + throw new XRPCError("InvalidRequest", "Missing text input"); 877 + } 878 + const subject = typeof body.subject === "string" ? body.subject : undefined; 879 + if (!subject) { 880 + throw new XRPCError("InvalidRequest", "Missing subject input"); 881 + } 882 + const focus = typeof body.focus === "string" ? body.focus : undefined; 883 + const replyTo = typeof body.replyTo === "string" ? body.replyTo : undefined; 884 + return { text, subject, focus, replyTo }; 885 + } 886 + 887 + async function parseDeleteCommentInputs( 888 + req: Request, 889 + ): Promise<DeleteCommentInputSchema> { 890 + const body = await req.json(); 891 + const uri = typeof body.uri === "string" ? body.uri : undefined; 892 + if (!uri) { 893 + throw new XRPCError("InvalidRequest", "Missing uri input"); 894 + } 895 + return { uri }; 896 + } 897 + 898 + async function parseUpdateProfileInputs( 899 + req: Request, 900 + ): Promise<UpdateProfileInputSchema> { 901 + const body = await req.json(); 902 + const displayName = typeof body.displayName === "string" 903 + ? body.displayName 904 + : undefined; 905 + if (!displayName) { 906 + throw new XRPCError("InvalidRequest", "Missing displayName input"); 907 + } 908 + const description = typeof body.description === "string" 909 + ? body.description 910 + : undefined; 911 + return { displayName, description }; 912 + } 913 + 914 + async function parseApplyAltsInputs( 915 + req: Request, 916 + ): Promise<ApplyAltsInputSchema> { 917 + const body = await req.json(); 918 + if (!body || typeof body !== "object" || !Array.isArray(body.writes)) { 919 + throw new XRPCError("InvalidRequest", "Missing or invalid writes array"); 920 + } 921 + const writes = Array.isArray(body.writes) 922 + ? body.writes.filter( 923 + (item: unknown): item is { photoUri: string; alt: string } => 924 + typeof item === "object" && 925 + item !== null && 926 + typeof (item as { photoUri?: unknown }).photoUri === "string" && 927 + typeof (item as { alt?: unknown }).alt === "string", 928 + ) 929 + : []; 930 + return { writes }; 931 + } 932 + 933 + async function parseApplySortInputs( 934 + req: Request, 935 + ): Promise<ApplySortInputSchema> { 936 + const body = await req.json(); 937 + const writes = Array.isArray(body.writes) && body.writes.every( 938 + (item: unknown): item is { itemUri: string; position: number } => 939 + typeof item === "object" && 940 + item !== null && 941 + typeof (item as { itemUri?: unknown }).itemUri === "string" && 942 + typeof (item as { position?: unknown }).position === "number", 943 + ) 944 + ? body.writes 945 + : []; 946 + return { writes }; 947 + } 948 + 949 + async function parseExifInputs( 950 + req: Request, 951 + ): Promise<{ 952 + photo: string; 953 + dateTimeOriginal?: string; 954 + exposureTime?: number; 955 + fNumber?: number; 956 + flash?: string; 957 + focalLengthIn35mmFormat?: number; 958 + iSO?: number; 959 + lensMake?: string; 960 + lensModel?: string; 961 + make?: string; 962 + model?: string; 963 + }> { 964 + const body = await req.json(); 965 + const photo = typeof body.photo === "string" ? body.photo : undefined; 966 + if (!photo) { 967 + throw new XRPCError("InvalidRequest", "Missing photo input"); 968 + } 969 + const dateTimeOriginal = typeof body.dateTimeOriginal === "string" 970 + ? body.dateTimeOriginal 971 + : undefined; 972 + const exposureTime = typeof body.exposureTime === "number" 973 + ? body.exposureTime 974 + : undefined; 975 + const fNumber = typeof body.fNumber === "number" ? body.fNumber : undefined; 976 + const flash = typeof body.flash === "string" ? body.flash : undefined; 977 + const focalLengthIn35mmFormat = 978 + typeof body.focalLengthIn35mmFormat === "number" 979 + ? body.focalLengthIn35mmFormat 980 + : undefined; 981 + const iSO = typeof body.iSO === "number" ? body.iSO : undefined; 982 + const lensMake = typeof body.lensMake === "string" 983 + ? body.lensMake 984 + : undefined; 985 + const lensModel = typeof body.lensModel === "string" 986 + ? body.lensModel 987 + : undefined; 988 + const make = typeof body.make === "string" ? body.make : undefined; 989 + const model = typeof body.model === "string" ? body.model : undefined; 990 + 991 + return { 992 + photo, 993 + dateTimeOriginal, 994 + exposureTime, 995 + fNumber, 996 + flash, 997 + focalLengthIn35mmFormat, 998 + iSO, 999 + lensMake, 1000 + lensModel, 1001 + make, 1002 + model, 1003 + }; 1004 + }
+27 -1
src/lib/actor.ts
··· 14 14 import { Record as PhotoExif } from "$lexicon/types/social/grain/photo/exif.ts"; 15 15 import { $Typed } from "$lexicon/util.ts"; 16 16 import { BffContext, WithBffMeta } from "@bigmoves/bff"; 17 - import { getFollow, getFollowersCount, getFollowsCount } from "./follow.ts"; 18 17 import { 19 18 galleryToView, 20 19 getGalleryCameras, 21 20 getGalleryItemsAndPhotos, 22 21 } from "./gallery.ts"; 22 + import { getFollow, getFollowersCount, getFollowsCount } from "./graph.ts"; 23 23 import { photoToView, photoUrl } from "./photo.ts"; 24 24 import type { SocialNetwork } from "./timeline.ts"; 25 25 ··· 418 418 419 419 return profileViews; 420 420 } 421 + 422 + export async function updateActorProfile( 423 + did: string, 424 + ctx: BffContext, 425 + params: { 426 + displayName?: string; 427 + description?: string; 428 + avatar?: GrainProfile["avatar"]; 429 + }, 430 + ) { 431 + const record = ctx.indexService.getRecord<WithBffMeta<GrainProfile>>( 432 + `at://${did}/social.grain.actor.profile/self`, 433 + ); 434 + if (!record) return null; 435 + 436 + const updated = await ctx.updateRecord<GrainProfile>( 437 + "social.grain.actor.profile", 438 + "self", 439 + { 440 + displayName: params.displayName ?? record.displayName, 441 + description: params.description ?? record.description, 442 + avatar: params.avatar ?? record.avatar, 443 + }, 444 + ); 445 + return updated; 446 + }
+48
src/lib/errors.ts
··· 8 8 }); 9 9 } 10 10 11 + function jsonErrorResponse(err: XRPCError): Response { 12 + return new Response(JSON.stringify(err.toJSON()), { 13 + status: err.status, 14 + headers: { 15 + "Content-Type": "application/json", 16 + }, 17 + }); 18 + } 19 + 11 20 export function onError(err: unknown): Response { 21 + if (err instanceof XRPCError) { 22 + return jsonErrorResponse(err); 23 + } 12 24 if (err instanceof BadRequestError) { 13 25 return errorResponse(err.message, 400); 14 26 } ··· 64 76 this.name = "BadRequestError"; 65 77 } 66 78 } 79 + 80 + const XRPCErrorCodes = { 81 + InvalidRequest: 400, 82 + NotFound: 404, 83 + InternalServerError: 500, 84 + AuthenticationRequired: 401, 85 + PayloadTooLarge: 413, 86 + } as const; 87 + 88 + type XRPCErrorCode = keyof typeof XRPCErrorCodes; 89 + 90 + export class XRPCError extends Error { 91 + code: XRPCErrorCode; 92 + error?: unknown; 93 + 94 + constructor(code: XRPCErrorCode, error?: unknown) { 95 + super(typeof error === "string" ? error : code); 96 + this.name = "XRPCError"; 97 + this.code = code; 98 + this.error = error; 99 + if (Error.captureStackTrace) { 100 + Error.captureStackTrace(this, XRPCError); 101 + } 102 + } 103 + 104 + get status(): number { 105 + return XRPCErrorCodes[this.code]; 106 + } 107 + 108 + toJSON() { 109 + return { 110 + error: this.code, 111 + message: this.message, 112 + }; 113 + } 114 + }
+12
src/lib/favs.ts
··· 1 + import { Record as Favorite } from "$lexicon/types/social/grain/favorite.ts"; 2 + import { BffContext, WithBffMeta } from "@bigmoves/bff"; 3 + 4 + export function createFavorite( 5 + subject: string, 6 + ctx: BffContext, 7 + ): Promise<string> { 8 + return ctx.createRecord<WithBffMeta<Favorite>>("social.grain.favorite", { 9 + subject, 10 + createdAt: new Date().toISOString(), 11 + }); 12 + }
+16
src/lib/follow.ts src/lib/graph.ts
··· 122 122 }, 123 123 ); 124 124 } 125 + 126 + export async function createFollow( 127 + followerDid: string, 128 + followeeDid: string, 129 + ctx: BffContext, 130 + ): Promise<string> { 131 + const followUri = await ctx.createRecord<GrainFollow>( 132 + "social.grain.graph.follow", 133 + { 134 + did: followerDid, 135 + subject: followeeDid, 136 + createdAt: new Date().toISOString(), 137 + }, 138 + ); 139 + return followUri; 140 + }
+192 -17
src/lib/gallery.ts
··· 18 18 } from "$lexicon/types/social/grain/photo/defs.ts"; 19 19 import { Record as PhotoExif } from "$lexicon/types/social/grain/photo/exif.ts"; 20 20 import { $Typed, Un$Typed } from "$lexicon/util.ts"; 21 + import { Facet } from "@atproto/api"; 21 22 import { AtUri } from "@atproto/syntax"; 22 23 import { BffContext, WithBffMeta } from "@bigmoves/bff"; 23 24 import { getGalleryCommentsCount } from "../modules/comments.tsx"; 24 25 import { getActorProfile, getActorProfilesBulk } from "./actor.ts"; 25 26 import { photoToView } from "./photo.ts"; 27 + import { parseFacetedText } from "./rich_text.ts"; 26 28 27 29 type PhotoWithMeta = WithBffMeta<Photo> & { 28 30 item?: WithBffMeta<GalleryItem>; ··· 144 146 } 145 147 146 148 export async function deleteGallery(uri: string, ctx: BffContext) { 147 - await ctx.deleteRecord(uri); 148 - const { items: galleryItems } = ctx.indexService.getRecords< 149 - WithBffMeta<GalleryItem> 150 - >("social.grain.gallery.item", { 151 - where: [{ field: "gallery", equals: uri }], 152 - }); 153 - for (const item of galleryItems) { 154 - await ctx.deleteRecord(item.uri); 155 - } 156 - const { items: favs } = ctx.indexService.getRecords<WithBffMeta<Favorite>>( 157 - "social.grain.favorite", 158 - { 159 - where: [{ field: "subject", equals: uri }], 160 - }, 161 - ); 162 - for (const fav of favs) { 163 - await ctx.deleteRecord(fav.uri); 149 + try { 150 + await ctx.deleteRecord(uri); 151 + const { items: galleryItems } = ctx.indexService.getRecords< 152 + WithBffMeta<GalleryItem> 153 + >("social.grain.gallery.item", { 154 + where: [{ field: "gallery", equals: uri }], 155 + }); 156 + for (const item of galleryItems) { 157 + await ctx.deleteRecord(item.uri); 158 + } 159 + const { items: favs } = ctx.indexService.getRecords<WithBffMeta<Favorite>>( 160 + "social.grain.favorite", 161 + { 162 + where: [{ field: "subject", equals: uri }], 163 + }, 164 + ); 165 + for (const fav of favs) { 166 + await ctx.deleteRecord(fav.uri); 167 + } 168 + } catch (error) { 169 + console.error("Failed to delete gallery:", error); 170 + return false; 164 171 } 172 + return true; 165 173 } 166 174 167 175 export function getGalleryFavs(galleryUri: string, ctx: BffContext) { ··· 470 478 ctx, 471 479 ); 472 480 } 481 + 482 + export function createGallery( 483 + ctx: BffContext, 484 + { 485 + title, 486 + description, 487 + }: { 488 + title: string; 489 + description?: string; 490 + }, 491 + ): Promise<string> { 492 + let facets: Facet[] | undefined = undefined; 493 + if (description) { 494 + try { 495 + const resp = parseFacetedText(description, ctx); 496 + facets = resp.facets; 497 + } catch (e) { 498 + console.error("Failed to parse facets:", e); 499 + } 500 + } 501 + 502 + return ctx.createRecord<Gallery>( 503 + "social.grain.gallery", 504 + { 505 + title, 506 + description, 507 + facets, 508 + updatedAt: new Date().toISOString(), 509 + createdAt: new Date().toISOString(), 510 + }, 511 + ); 512 + } 513 + 514 + export async function updateGallery( 515 + ctx: BffContext, 516 + uri: string, 517 + { 518 + title, 519 + description, 520 + }: { 521 + title: string; 522 + description?: string; 523 + }, 524 + ): Promise<boolean> { 525 + let facets: Facet[] | undefined = undefined; 526 + if (description) { 527 + try { 528 + const resp = parseFacetedText(description, ctx); 529 + facets = resp.facets; 530 + } catch (e) { 531 + console.error("Failed to parse facets:", e); 532 + } 533 + } 534 + 535 + const gallery = ctx.indexService.getRecord<WithBffMeta<Gallery>>(uri); 536 + if (!gallery) return false; 537 + const rkey = new AtUri(uri).rkey; 538 + await ctx.updateRecord<Gallery>( 539 + "social.grain.gallery", 540 + rkey, 541 + { 542 + title, 543 + description, 544 + facets, 545 + updatedAt: new Date().toISOString(), 546 + createdAt: gallery.createdAt, 547 + }, 548 + ); 549 + return true; 550 + } 551 + 552 + export async function createGalleryItem( 553 + ctx: BffContext, 554 + galleryUri: string, 555 + itemUri: string, 556 + ): Promise<string | null> { 557 + const count = ctx.indexService.countRecords( 558 + "social.grain.gallery.item", 559 + { 560 + where: [ 561 + { field: "gallery", equals: galleryUri }, 562 + ], 563 + }, 564 + ); 565 + const position = count ?? 0; 566 + const createdItemUri = await ctx.createRecord<GalleryItem>( 567 + "social.grain.gallery.item", 568 + { 569 + gallery: galleryUri, 570 + item: itemUri, 571 + position, 572 + createdAt: new Date().toISOString(), 573 + }, 574 + ); 575 + return createdItemUri; 576 + } 577 + 578 + export async function removeGalleryItem( 579 + ctx: BffContext, 580 + galleryUri: string, 581 + photoUri: string, 582 + ): Promise<boolean> { 583 + const { items: galleryItems } = ctx.indexService.getRecords< 584 + WithBffMeta<GalleryItem> 585 + >( 586 + "social.grain.gallery.item", 587 + { 588 + where: [ 589 + { field: "gallery", equals: galleryUri }, 590 + { field: "item", equals: photoUri }, 591 + ], 592 + }, 593 + ); 594 + if (!galleryItems.length) return false; 595 + for (const item of galleryItems) { 596 + await ctx.deleteRecord(item.uri); 597 + } 598 + return true; 599 + } 600 + 601 + export async function applySort( 602 + writes: Array<{ 603 + itemUri: string; 604 + position: number; 605 + }>, 606 + ctx: BffContext, 607 + ): Promise<boolean> { 608 + const urisToUpdate = writes.map((update) => update.itemUri); 609 + 610 + const { items: galleryItems } = ctx.indexService.getRecords< 611 + WithBffMeta<GalleryItem> 612 + >( 613 + "social.grain.gallery.item", 614 + { 615 + where: [ 616 + { field: "uri", in: urisToUpdate }, 617 + ], 618 + }, 619 + ); 620 + 621 + const positionMap = new Map<string, number>(); 622 + for (const update of writes) { 623 + positionMap.set(update.itemUri, update.position); 624 + } 625 + 626 + const updates = galleryItems.map((item) => { 627 + // Use position from positionMap if it exists (including 0) 628 + const hasPosition = positionMap.has(item.uri); 629 + return { 630 + collection: "social.grain.gallery.item", 631 + rkey: new AtUri(item.uri).rkey, 632 + data: { 633 + ...item, 634 + position: hasPosition ? positionMap.get(item.uri) : item.position, 635 + }, 636 + }; 637 + }); 638 + 639 + try { 640 + await ctx.updateRecords(updates); 641 + } catch (error) { 642 + console.error("Failed to update gallery item positions:", error); 643 + return false; 644 + } 645 + 646 + return true; 647 + }
+70
src/lib/photo.ts
··· 5 5 import { ExifView, PhotoView } from "$lexicon/types/social/grain/photo/defs.ts"; 6 6 import { Record as PhotoExif } from "$lexicon/types/social/grain/photo/exif.ts"; 7 7 import { $Typed } from "$lexicon/util.ts"; 8 + import { AtUri } from "@atproto/syntax"; 8 9 import { BffContext, WithBffMeta } from "@bigmoves/bff"; 9 10 import { format, parseISO } from "date-fns"; 10 11 import { PUBLIC_URL, USE_CDN } from "../env.ts"; ··· 280 281 photoToView(photo.did, photo, exifMap.get(photo.uri)) 281 282 ); 282 283 } 284 + 285 + export async function createPhoto( 286 + data: Partial<Photo>, 287 + ctx: BffContext, 288 + ): Promise<string> { 289 + const photoUri = await ctx.createRecord<Photo>( 290 + "social.grain.photo", 291 + { 292 + photo: data.photo, 293 + alt: data.alt || "", 294 + aspectRatio: data.aspectRatio || undefined, 295 + createdAt: new Date().toISOString(), 296 + }, 297 + ); 298 + return photoUri; 299 + } 300 + 301 + export async function createExif( 302 + data: Partial<PhotoExif>, 303 + ctx: BffContext, 304 + ): Promise<string> { 305 + const exifUri = await ctx.createRecord<PhotoExif>( 306 + "social.grain.photo.exif", 307 + { 308 + ...data, 309 + createdAt: new Date().toISOString(), 310 + }, 311 + ); 312 + return exifUri; 313 + } 314 + 315 + export async function applyAlts( 316 + writes: Array<{ 317 + photoUri: string; 318 + alt: string; 319 + }>, 320 + ctx: BffContext, 321 + ): Promise<boolean> { 322 + const altMap = new Map<string, string>(); 323 + for (const update of writes) { 324 + altMap.set(update.photoUri, update.alt); 325 + } 326 + 327 + const urisToUpdate = writes.map((update) => update.photoUri); 328 + 329 + const { items: photoRecords } = ctx.indexService.getRecords< 330 + WithBffMeta<Photo> 331 + >( 332 + "social.grain.photo", 333 + { 334 + where: [{ field: "uri", in: urisToUpdate }], 335 + }, 336 + ); 337 + 338 + const updates = photoRecords.map((record) => ({ 339 + collection: "social.grain.photo", 340 + rkey: new AtUri(record.uri).rkey, 341 + data: { ...record, alt: altMap.get(record.uri) ?? record.alt ?? undefined }, 342 + })); 343 + 344 + try { 345 + await ctx.updateRecords(updates); 346 + } catch (error) { 347 + console.error("Failed to update photo alts:", error); 348 + return false; 349 + } 350 + 351 + return true; 352 + }
+28 -29
src/modules/comments.tsx
··· 326 326 ); 327 327 } 328 328 329 + export function createComment( 330 + data: Partial<Comment>, 331 + ctx: BffContext, 332 + ): Promise<string> { 333 + let facets: Facet[] | undefined = undefined; 334 + const resp = parseFacetedText(data.text ?? "", ctx); 335 + facets = resp.facets; 336 + return ctx.createRecord<WithBffMeta<Comment>>( 337 + "social.grain.comment", 338 + { 339 + ...data, 340 + facets, 341 + createdAt: new Date().toISOString(), 342 + }, 343 + ); 344 + } 345 + 329 346 export const middlewares: BffMiddleware[] = [ 330 347 // Actions 331 348 route( ··· 344 361 345 362 const form = await req.formData(); 346 363 const text = form.get("text") as string; 347 - const focus = form.get("focus") as string; 348 - const replyTo = form.get("replyTo") as string; 364 + const focus = form.get("focus") as string ?? undefined; 365 + const replyTo = form.get("replyTo") as string ?? undefined; 349 366 350 367 if (typeof text !== "string" || text.length === 0) { 351 368 return new Response("Text is required", { status: 400 }); 352 369 } 353 - 354 - let facets: Facet[] | undefined = undefined; 355 - if (text) { 356 - try { 357 - const resp = parseFacetedText(text, ctx); 358 - facets = resp.facets; 359 - } catch (e) { 360 - console.error("Failed to parse facets:", e); 361 - } 362 - } 363 - 364 370 try { 365 - await ctx.createRecord<WithBffMeta<Comment>>( 366 - "social.grain.comment", 367 - { 368 - text, 369 - facets, 370 - subject: gallery.uri, 371 - focus: focus ?? undefined, 372 - replyTo: replyTo ?? undefined, 373 - createdAt: new Date().toISOString(), 374 - }, 375 - ); 371 + await createComment({ 372 + subject: gallery.uri, 373 + text, 374 + focus, 375 + replyTo, 376 + }, ctx); 376 377 } catch (error) { 377 - console.error("Error creating comment:", error); 378 - if (error instanceof Error) { 379 - throw new BadRequestError(error.message); 380 - } else { 381 - throw new BadRequestError("Unknown error"); 378 + if (error instanceof BadRequestError) { 379 + return new Response(error.message, { status: 400 }); 382 380 } 381 + throw error; 383 382 } 384 383 385 384 const comments = getGalleryComments(gallery.uri, ctx);
+35 -74
src/routes/actions.tsx
··· 1 1 import { Record as BskyFollow } from "$lexicon/types/app/bsky/graph/follow.ts"; 2 2 import { Record as Profile } from "$lexicon/types/social/grain/actor/profile.ts"; 3 3 import { Record as Favorite } from "$lexicon/types/social/grain/favorite.ts"; 4 - import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts"; 5 4 import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts"; 6 5 import { Record as GalleryItem } from "$lexicon/types/social/grain/gallery/item.ts"; 7 6 import { Record as Photo } from "$lexicon/types/social/grain/photo.ts"; 8 7 import { isPhotoView } from "$lexicon/types/social/grain/photo/defs.ts"; 9 8 import { Record as PhotoExif } from "$lexicon/types/social/grain/photo/exif.ts"; 10 - import { Facet } from "@atproto/api"; 11 9 import { AtUri } from "@atproto/syntax"; 12 10 import { BffContext, RouteHandler, WithBffMeta } from "@bigmoves/bff"; 13 11 import { ··· 21 19 import { PhotoPreview } from "../components/PhotoPreview.tsx"; 22 20 import { PhotoSelectButton } from "../components/PhotoSelectButton.tsx"; 23 21 import { getActorPhotos } from "../lib/actor.ts"; 24 - import { getFollowers } from "../lib/follow.ts"; 25 - import { deleteGallery, getGallery, getGalleryFav } from "../lib/gallery.ts"; 26 - import { getPhoto, photoToView } from "../lib/photo.ts"; 27 - import { parseFacetedText } from "../lib/rich_text.ts"; 22 + import { 23 + createGallery, 24 + createGalleryItem, 25 + deleteGallery, 26 + getGallery, 27 + getGalleryFav, 28 + updateGallery, 29 + } from "../lib/gallery.ts"; 30 + import { getFollowers } from "../lib/graph.ts"; 31 + import { createExif, createPhoto, getPhoto } from "../lib/photo.ts"; 28 32 import type { State } from "../state.ts"; 29 33 import { galleryLink, profileLink, uploadPageLink } from "../utils.ts"; 30 34 ··· 99 103 const searchParams = new URLSearchParams(url.search); 100 104 const uri = searchParams.get("uri"); 101 105 102 - let facets: Facet[] | undefined = undefined; 103 - if (description) { 104 - try { 105 - const resp = parseFacetedText(description, ctx); 106 - facets = resp.facets; 107 - } catch (e) { 108 - console.error("Failed to parse facets:", e); 109 - } 110 - } 111 - 112 106 if (uri) { 113 - const gallery = ctx.indexService.getRecord<WithBffMeta<Gallery>>(uri); 114 - if (!gallery) return ctx.next(); 107 + const success = await updateGallery(ctx, uri, { 108 + title, 109 + description, 110 + }); 111 + if (!success) { 112 + return new Response("Failed to update gallery", { status: 500 }); 113 + } 115 114 const rkey = new AtUri(uri).rkey; 116 - try { 117 - await ctx.updateRecord<Gallery>("social.grain.gallery", rkey, { 118 - title, 119 - description, 120 - facets, 121 - updatedAt: new Date().toISOString(), 122 - createdAt: gallery.createdAt, 123 - }); 124 - } catch (e) { 125 - console.error("Error updating record:", e); 126 - const errorMessage = e instanceof Error 127 - ? e.message 128 - : "Unknown error occurred"; 129 - return new Response(errorMessage, { status: 400 }); 130 - } 131 115 return ctx.redirect(galleryLink(handle, rkey)); 132 116 } 133 117 134 - const createdUri = await ctx.createRecord<Gallery>( 135 - "social.grain.gallery", 136 - { 137 - title, 138 - description, 139 - facets, 140 - updatedAt: new Date().toISOString(), 141 - createdAt: new Date().toISOString(), 142 - }, 143 - ); 144 - return ctx.redirect(galleryLink(handle, new AtUri(createdUri).rkey)); 118 + const galleryUri = await createGallery(ctx, { title, description }); 119 + if (!galleryUri) { 120 + return new Response("Failed to create gallery", { status: 500 }); 121 + } 122 + const rkey = new AtUri(galleryUri).rkey; 123 + return ctx.redirect(galleryLink(handle, rkey)); 145 124 }; 146 125 147 126 export const galleryDelete: RouteHandler = async ( ··· 658 637 659 638 const blobResponse = await ctx.agent.uploadBlob(file); 660 639 661 - const photoUri = await ctx.createRecord<Photo>("social.grain.photo", { 640 + const photoUri = await createPhoto({ 662 641 photo: blobResponse.data.blob, 663 642 aspectRatio: width && height 664 643 ? { ··· 668 647 : undefined, 669 648 alt: "", 670 649 createdAt: new Date().toISOString(), 671 - }); 650 + }, ctx); 672 651 673 - let exifUri: string | undefined = undefined; 674 652 if (exif) { 675 - exifUri = await ctx.createRecord<PhotoExif>( 676 - "social.grain.photo.exif", 653 + await createExif( 677 654 { 678 655 photo: photoUri, 656 + ...exif, 679 657 createdAt: new Date().toISOString(), 680 - ...exif, 681 658 }, 659 + ctx, 682 660 ); 683 661 } 684 662 685 - const photo = ctx.indexService.getRecord<WithBffMeta<Photo>>(photoUri); 686 - if (!photo) { 687 - return new Response("Photo not found after creation", { status: 404 }); 688 - } 689 - 690 663 let gallery: GalleryView | undefined = undefined; 691 664 if (galleryUri) { 692 665 gallery = getGallery(did, new AtUri(galleryUri).rkey, ctx) ?? undefined; 693 - if (gallery) { 694 - await ctx.createRecord<GalleryItem>("social.grain.gallery.item", { 695 - gallery: galleryUri, 696 - item: photoUri, 697 - position: gallery.items?.length ?? 0, 698 - createdAt: new Date().toISOString(), 699 - }); 700 - } 666 + await createGalleryItem(ctx, galleryUri, photoUri); 701 667 } 702 668 703 - let exifRecord: WithBffMeta<PhotoExif> | undefined = undefined; 704 - if (exifUri) { 705 - exifRecord = ctx.indexService.getRecord<WithBffMeta<PhotoExif>>( 706 - exifUri, 707 - ); 708 - } 669 + const photo = getPhoto(photoUri, ctx); 670 + if (!photo) return ctx.next(); 709 671 710 672 if (page === "gallery" && gallery && galleryUri) { 711 673 const rkey = new AtUri(gallery.uri).rkey; ··· 714 676 if (!updatedGallery) { 715 677 return ctx.next(); 716 678 } 717 - const p = photoToView(did, photo, exifRecord); 718 679 return ctx.html( 719 680 <> 720 681 <PhotoSelectButton 721 682 galleryUri={gallery.uri} 722 - photo={p} 683 + photo={photo} 723 684 /> 724 685 <div hx-swap-oob="beforeend:#gallery-container"> 725 686 <GalleryLayout.Item 726 687 key={photo.cid} 727 - photo={p} 688 + photo={photo} 728 689 gallery={updatedGallery} 729 690 /> 730 691 </div> ··· 749 710 return ctx.html( 750 711 <> 751 712 <PhotoPreview 752 - photo={photoToView(did, photo, exifRecord)} 713 + photo={photo} 753 714 selectedGallery={gallery} 754 715 /> 755 716 <div hx-swap-oob="photos-count">{photosCount}</div> 756 717 </>, 757 718 ); 758 719 } catch (e) { 759 - console.error("Error in uploadStart:", e); 720 + console.error("Error in uploadPhoto:", e); 760 721 return new Response("Internal Server Error", { status: 500 }); 761 722 } 762 723 };
+1 -1
src/routes/followers.tsx
··· 3 3 import { FollowsList } from "../components/FollowsList.tsx"; 4 4 import { Header } from "../components/Header.tsx"; 5 5 import { getActorProfile } from "../lib/actor.ts"; 6 - import { getFollowersWithProfiles } from "../lib/follow.ts"; 6 + import { getFollowersWithProfiles } from "../lib/graph.ts"; 7 7 import { State } from "../state.ts"; 8 8 import { profileLink } from "../utils.ts"; 9 9
+1 -1
src/routes/follows.tsx
··· 3 3 import { FollowsList } from "../components/FollowsList.tsx"; 4 4 import { Header } from "../components/Header.tsx"; 5 5 import { getActorProfile } from "../lib/actor.ts"; 6 - import { getFollowingWithProfiles } from "../lib/follow.ts"; 6 + import { getFollowingWithProfiles } from "../lib/graph.ts"; 7 7 import { State } from "../state.ts"; 8 8 import { profileLink } from "../utils.ts"; 9 9