atmo.rsvp
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

kinda working

Florian 03cfc491 b40d179a

+4610 -816
+16 -1
lexicons-generated/community/lexicon/calendar/event/getRecord.json lexicons-generated/rsvp/atmo/event/getRecord.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "community.lexicon.calendar.event.getRecord", 3 + "id": "rsvp.atmo.event.getRecord", 4 4 "defs": { 5 5 "main": { 6 6 "type": "query", ··· 19 19 "profiles": { 20 20 "type": "boolean", 21 21 "description": "Include profile + identity info keyed by DID" 22 + }, 23 + "spaceUri": { 24 + "type": "string", 25 + "format": "at-uri", 26 + "description": "If set, fetch from this permissioned space (requires service-auth JWT)." 22 27 }, 23 28 "hydrateRsvps": { 24 29 "type": "integer", ··· 65 70 "time_us": { 66 71 "type": "integer" 67 72 }, 73 + "space": { 74 + "type": "string", 75 + "format": "at-uri", 76 + "description": "Present when the record was read from a permissioned space; its value is the space URI." 77 + }, 68 78 "rsvpsCount": { 69 79 "type": "integer", 70 80 "description": "Total rsvps count" ··· 130 140 }, 131 141 "time_us": { 132 142 "type": "integer" 143 + }, 144 + "space": { 145 + "type": "string", 146 + "format": "at-uri", 147 + "description": "Present when the record was read from a permissioned space." 133 148 } 134 149 } 135 150 },
+21 -1
lexicons-generated/community/lexicon/calendar/event/listRecords.json lexicons-generated/rsvp/atmo/event/listRecords.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "community.lexicon.calendar.event.listRecords", 3 + "id": "rsvp.atmo.event.listRecords", 4 4 "defs": { 5 5 "main": { 6 6 "type": "query", ··· 25 25 "profiles": { 26 26 "type": "boolean", 27 27 "description": "Include profile + identity info keyed by DID" 28 + }, 29 + "spaceUri": { 30 + "type": "string", 31 + "format": "at-uri", 32 + "description": "If set, query records inside this permissioned space (requires service-auth JWT)." 33 + }, 34 + "byUser": { 35 + "type": "string", 36 + "format": "did", 37 + "description": "Only used with spaceUri — filter to records authored by this DID." 28 38 }, 29 39 "search": { 30 40 "type": "string", ··· 183 193 "time_us": { 184 194 "type": "integer" 185 195 }, 196 + "space": { 197 + "type": "string", 198 + "format": "at-uri", 199 + "description": "Present when the record was read from a permissioned space; its value is the space URI." 200 + }, 186 201 "rsvpsCount": { 187 202 "type": "integer", 188 203 "description": "Total rsvps count" ··· 239 254 }, 240 255 "time_us": { 241 256 "type": "integer" 257 + }, 258 + "space": { 259 + "type": "string", 260 + "format": "at-uri", 261 + "description": "Present when the record was read from a permissioned space." 242 262 } 243 263 } 244 264 },
+16 -1
lexicons-generated/community/lexicon/calendar/rsvp/getRecord.json lexicons-generated/rsvp/atmo/rsvp/getRecord.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "community.lexicon.calendar.rsvp.getRecord", 3 + "id": "rsvp.atmo.rsvp.getRecord", 4 4 "defs": { 5 5 "main": { 6 6 "type": "query", ··· 19 19 "profiles": { 20 20 "type": "boolean", 21 21 "description": "Include profile + identity info keyed by DID" 22 + }, 23 + "spaceUri": { 24 + "type": "string", 25 + "format": "at-uri", 26 + "description": "If set, fetch from this permissioned space (requires service-auth JWT)." 22 27 }, 23 28 "hydrateEvent": { 24 29 "type": "boolean", ··· 63 68 "time_us": { 64 69 "type": "integer" 65 70 }, 71 + "space": { 72 + "type": "string", 73 + "format": "at-uri", 74 + "description": "Present when the record was read from a permissioned space; its value is the space URI." 75 + }, 66 76 "event": { 67 77 "type": "ref", 68 78 "ref": "#refEventRecord" ··· 112 122 }, 113 123 "time_us": { 114 124 "type": "integer" 125 + }, 126 + "space": { 127 + "type": "string", 128 + "format": "at-uri", 129 + "description": "Present when the record was read from a permissioned space." 115 130 } 116 131 } 117 132 },
+21 -1
lexicons-generated/community/lexicon/calendar/rsvp/listRecords.json lexicons-generated/rsvp/atmo/rsvp/listRecords.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "community.lexicon.calendar.rsvp.listRecords", 3 + "id": "rsvp.atmo.rsvp.listRecords", 4 4 "defs": { 5 5 "main": { 6 6 "type": "query", ··· 25 25 "profiles": { 26 26 "type": "boolean", 27 27 "description": "Include profile + identity info keyed by DID" 28 + }, 29 + "spaceUri": { 30 + "type": "string", 31 + "format": "at-uri", 32 + "description": "If set, query records inside this permissioned space (requires service-auth JWT)." 33 + }, 34 + "byUser": { 35 + "type": "string", 36 + "format": "did", 37 + "description": "Only used with spaceUri — filter to records authored by this DID." 28 38 }, 29 39 "status": { 30 40 "type": "string", ··· 120 130 "time_us": { 121 131 "type": "integer" 122 132 }, 133 + "space": { 134 + "type": "string", 135 + "format": "at-uri", 136 + "description": "Present when the record was read from a permissioned space; its value is the space URI." 137 + }, 123 138 "event": { 124 139 "type": "ref", 125 140 "ref": "#refEventRecord" ··· 160 175 }, 161 176 "time_us": { 162 177 "type": "integer" 178 + }, 179 + "space": { 180 + "type": "string", 181 + "format": "at-uri", 182 + "description": "Present when the record was read from a permissioned space." 163 183 } 164 184 } 165 185 },
+14 -3
lexicons-generated/rsvp/atmo/getProfile.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "query", 7 - "description": "Get a user's profile by DID or handle", 7 + "description": "Get a user's profiles by DID or handle", 8 8 "parameters": { 9 9 "type": "params", 10 10 "required": [ ··· 21 21 "output": { 22 22 "encoding": "application/json", 23 23 "schema": { 24 - "type": "ref", 25 - "ref": "#profileEntry" 24 + "type": "object", 25 + "required": [ 26 + "profiles" 27 + ], 28 + "properties": { 29 + "profiles": { 30 + "type": "array", 31 + "items": { 32 + "type": "ref", 33 + "ref": "#profileEntry" 34 + } 35 + } 36 + } 26 37 } 27 38 } 28 39 },
+42
lexicons-generated/rsvp/atmo/permissionSet.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.permissionSet", 4 + "defs": { 5 + "main": { 6 + "type": "permission-set", 7 + "title": "Atmo Events", 8 + "description": "Manage your private events and rsvps.", 9 + "permissions": [ 10 + { 11 + "type": "permission", 12 + "resource": "rpc", 13 + "aud": "*", 14 + "lxm": [ 15 + "rsvp.atmo.event.getRecord", 16 + "rsvp.atmo.event.listRecords", 17 + "rsvp.atmo.getCursor", 18 + "rsvp.atmo.getOverview", 19 + "rsvp.atmo.getProfile", 20 + "rsvp.atmo.notifyOfUpdate", 21 + "rsvp.atmo.rsvp.getRecord", 22 + "rsvp.atmo.rsvp.listRecords", 23 + "rsvp.atmo.space.admin.addMember", 24 + "rsvp.atmo.space.admin.createSpace", 25 + "rsvp.atmo.space.admin.removeMember", 26 + "rsvp.atmo.space.deleteRecord", 27 + "rsvp.atmo.space.getRecord", 28 + "rsvp.atmo.space.getSpace", 29 + "rsvp.atmo.space.invite.create", 30 + "rsvp.atmo.space.invite.list", 31 + "rsvp.atmo.space.invite.redeem", 32 + "rsvp.atmo.space.invite.revoke", 33 + "rsvp.atmo.space.listMembers", 34 + "rsvp.atmo.space.listRecords", 35 + "rsvp.atmo.space.listSpaces", 36 + "rsvp.atmo.space.putRecord" 37 + ] 38 + } 39 + ] 40 + } 41 + } 42 + }
+60
lexicons-generated/rsvp/atmo/space/admin/addMember.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.admin.addMember", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Add a member to a space. Caller must be the space owner.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "did" 15 + ], 16 + "properties": { 17 + "spaceUri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "did": { 22 + "type": "string", 23 + "format": "did" 24 + }, 25 + "perms": { 26 + "type": "string", 27 + "knownValues": [ 28 + "read", 29 + "write" 30 + ], 31 + "default": "write" 32 + } 33 + } 34 + } 35 + }, 36 + "output": { 37 + "encoding": "application/json", 38 + "schema": { 39 + "type": "object", 40 + "required": [ 41 + "ok" 42 + ], 43 + "properties": { 44 + "ok": { 45 + "type": "boolean" 46 + } 47 + } 48 + } 49 + }, 50 + "errors": [ 51 + { 52 + "name": "NotFound" 53 + }, 54 + { 55 + "name": "Forbidden" 56 + } 57 + ] 58 + } 59 + } 60 + }
+59
lexicons-generated/rsvp/atmo/space/admin/createSpace.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.admin.createSpace", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create a new space owned by the JWT issuer. The caller is added as an owner-perm member.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "type": { 14 + "type": "string", 15 + "format": "nsid", 16 + "description": "Space type NSID. Defaults to the service's configured type." 17 + }, 18 + "key": { 19 + "type": "string", 20 + "description": "Space key. Auto-generated (TID) if omitted." 21 + }, 22 + "memberListRef": { 23 + "type": "string", 24 + "format": "at-uri" 25 + }, 26 + "appPolicyRef": { 27 + "type": "string", 28 + "format": "at-uri" 29 + }, 30 + "appPolicy": { 31 + "type": "ref", 32 + "ref": "rsvp.atmo.space.defs#appPolicy" 33 + } 34 + } 35 + } 36 + }, 37 + "output": { 38 + "encoding": "application/json", 39 + "schema": { 40 + "type": "object", 41 + "required": [ 42 + "space" 43 + ], 44 + "properties": { 45 + "space": { 46 + "type": "ref", 47 + "ref": "rsvp.atmo.space.defs#spaceView" 48 + } 49 + } 50 + } 51 + }, 52 + "errors": [ 53 + { 54 + "name": "AlreadyExists" 55 + } 56 + ] 57 + } 58 + } 59 + }
+55
lexicons-generated/rsvp/atmo/space/admin/removeMember.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.admin.removeMember", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Remove a member from a space. Owner only. Cannot remove the owner.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "did" 15 + ], 16 + "properties": { 17 + "spaceUri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "did": { 22 + "type": "string", 23 + "format": "did" 24 + } 25 + } 26 + } 27 + }, 28 + "output": { 29 + "encoding": "application/json", 30 + "schema": { 31 + "type": "object", 32 + "required": [ 33 + "ok" 34 + ], 35 + "properties": { 36 + "ok": { 37 + "type": "boolean" 38 + } 39 + } 40 + } 41 + }, 42 + "errors": [ 43 + { 44 + "name": "NotFound" 45 + }, 46 + { 47 + "name": "Forbidden" 48 + }, 49 + { 50 + "name": "InvalidRequest" 51 + } 52 + ] 53 + } 54 + } 55 + }
+193
lexicons-generated/rsvp/atmo/space/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.defs", 4 + "description": "Shared types for permissioned-space XRPC methods.", 5 + "defs": { 6 + "spaceView": { 7 + "type": "object", 8 + "required": [ 9 + "uri", 10 + "ownerDid", 11 + "type", 12 + "key", 13 + "serviceDid", 14 + "createdAt" 15 + ], 16 + "properties": { 17 + "uri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "ownerDid": { 22 + "type": "string", 23 + "format": "did" 24 + }, 25 + "type": { 26 + "type": "string", 27 + "format": "nsid" 28 + }, 29 + "key": { 30 + "type": "string" 31 + }, 32 + "serviceDid": { 33 + "type": "string" 34 + }, 35 + "memberListRef": { 36 + "type": "string", 37 + "format": "at-uri" 38 + }, 39 + "appPolicyRef": { 40 + "type": "string", 41 + "format": "at-uri" 42 + }, 43 + "createdAt": { 44 + "type": "integer" 45 + }, 46 + "appPolicy": { 47 + "type": "ref", 48 + "ref": "#appPolicy", 49 + "description": "Owner-only" 50 + } 51 + } 52 + }, 53 + "memberView": { 54 + "type": "object", 55 + "required": [ 56 + "did", 57 + "perms", 58 + "addedAt" 59 + ], 60 + "properties": { 61 + "did": { 62 + "type": "string", 63 + "format": "did" 64 + }, 65 + "perms": { 66 + "type": "string", 67 + "knownValues": [ 68 + "read", 69 + "write" 70 + ], 71 + "description": "'write' implies 'read'. Space owner is always implicit write." 72 + }, 73 + "addedAt": { 74 + "type": "integer" 75 + }, 76 + "addedBy": { 77 + "type": "string", 78 + "format": "did" 79 + } 80 + } 81 + }, 82 + "recordView": { 83 + "type": "object", 84 + "required": [ 85 + "spaceUri", 86 + "collection", 87 + "authorDid", 88 + "rkey", 89 + "record", 90 + "createdAt" 91 + ], 92 + "properties": { 93 + "spaceUri": { 94 + "type": "string", 95 + "format": "at-uri" 96 + }, 97 + "collection": { 98 + "type": "string", 99 + "format": "nsid" 100 + }, 101 + "authorDid": { 102 + "type": "string", 103 + "format": "did" 104 + }, 105 + "rkey": { 106 + "type": "string" 107 + }, 108 + "cid": { 109 + "type": "string", 110 + "format": "cid" 111 + }, 112 + "record": { 113 + "type": "unknown" 114 + }, 115 + "createdAt": { 116 + "type": "integer" 117 + } 118 + } 119 + }, 120 + "appPolicy": { 121 + "type": "object", 122 + "required": [ 123 + "mode", 124 + "apps" 125 + ], 126 + "properties": { 127 + "mode": { 128 + "type": "string", 129 + "knownValues": [ 130 + "allow", 131 + "deny" 132 + ], 133 + "description": "'allow' = default-allow with apps[] as denylist; 'deny' = default-deny with apps[] as allowlist." 134 + }, 135 + "apps": { 136 + "type": "array", 137 + "items": { 138 + "type": "string" 139 + } 140 + } 141 + } 142 + }, 143 + "inviteView": { 144 + "type": "object", 145 + "required": [ 146 + "tokenHash", 147 + "spaceUri", 148 + "perms", 149 + "usedCount", 150 + "createdBy", 151 + "createdAt" 152 + ], 153 + "properties": { 154 + "tokenHash": { 155 + "type": "string" 156 + }, 157 + "spaceUri": { 158 + "type": "string", 159 + "format": "at-uri" 160 + }, 161 + "perms": { 162 + "type": "string", 163 + "knownValues": [ 164 + "read", 165 + "write" 166 + ] 167 + }, 168 + "expiresAt": { 169 + "type": "integer" 170 + }, 171 + "maxUses": { 172 + "type": "integer" 173 + }, 174 + "usedCount": { 175 + "type": "integer" 176 + }, 177 + "createdBy": { 178 + "type": "string", 179 + "format": "did" 180 + }, 181 + "createdAt": { 182 + "type": "integer" 183 + }, 184 + "revokedAt": { 185 + "type": "integer" 186 + }, 187 + "note": { 188 + "type": "string" 189 + } 190 + } 191 + } 192 + } 193 + }
+56
lexicons-generated/rsvp/atmo/space/deleteRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.deleteRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete a record from a space. Callers can delete their own records; the space owner can delete any record (via a separate admin path).", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "collection", 15 + "rkey" 16 + ], 17 + "properties": { 18 + "spaceUri": { 19 + "type": "string", 20 + "format": "at-uri" 21 + }, 22 + "collection": { 23 + "type": "string", 24 + "format": "nsid" 25 + }, 26 + "rkey": { 27 + "type": "string" 28 + } 29 + } 30 + } 31 + }, 32 + "output": { 33 + "encoding": "application/json", 34 + "schema": { 35 + "type": "object", 36 + "required": [ 37 + "ok" 38 + ], 39 + "properties": { 40 + "ok": { 41 + "type": "boolean" 42 + } 43 + } 44 + } 45 + }, 46 + "errors": [ 47 + { 48 + "name": "NotFound" 49 + }, 50 + { 51 + "name": "Forbidden" 52 + } 53 + ] 54 + } 55 + } 56 + }
+59
lexicons-generated/rsvp/atmo/space/getRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.getRecord", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a single record from a space.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri", 12 + "collection", 13 + "author", 14 + "rkey" 15 + ], 16 + "properties": { 17 + "spaceUri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "collection": { 22 + "type": "string", 23 + "format": "nsid" 24 + }, 25 + "author": { 26 + "type": "string", 27 + "format": "did" 28 + }, 29 + "rkey": { 30 + "type": "string" 31 + } 32 + } 33 + }, 34 + "output": { 35 + "encoding": "application/json", 36 + "schema": { 37 + "type": "object", 38 + "required": [ 39 + "record" 40 + ], 41 + "properties": { 42 + "record": { 43 + "type": "ref", 44 + "ref": "rsvp.atmo.space.defs#recordView" 45 + } 46 + } 47 + } 48 + }, 49 + "errors": [ 50 + { 51 + "name": "NotFound" 52 + }, 53 + { 54 + "name": "Forbidden" 55 + } 56 + ] 57 + } 58 + } 59 + }
+45
lexicons-generated/rsvp/atmo/space/getSpace.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.getSpace", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get metadata for a single space. Caller must be a member or the owner.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "uri" 12 + ], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri" 17 + } 18 + } 19 + }, 20 + "output": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "required": [ 25 + "space" 26 + ], 27 + "properties": { 28 + "space": { 29 + "type": "ref", 30 + "ref": "rsvp.atmo.space.defs#spaceView" 31 + } 32 + } 33 + } 34 + }, 35 + "errors": [ 36 + { 37 + "name": "NotFound" 38 + }, 39 + { 40 + "name": "Forbidden" 41 + } 42 + ] 43 + } 44 + } 45 + }
+74
lexicons-generated/rsvp/atmo/space/invite/create.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.invite.create", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create an invite for a space. Caller must be the space owner. Returns the raw token once; only the hash is stored.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri" 14 + ], 15 + "properties": { 16 + "spaceUri": { 17 + "type": "string", 18 + "format": "at-uri" 19 + }, 20 + "perms": { 21 + "type": "string", 22 + "knownValues": [ 23 + "read", 24 + "write" 25 + ], 26 + "default": "write" 27 + }, 28 + "expiresAt": { 29 + "type": "integer", 30 + "description": "Unix ms timestamp. Omit for no expiry." 31 + }, 32 + "maxUses": { 33 + "type": "integer", 34 + "minimum": 1, 35 + "description": "Omit for unlimited uses." 36 + }, 37 + "note": { 38 + "type": "string", 39 + "maxLength": 500 40 + } 41 + } 42 + } 43 + }, 44 + "output": { 45 + "encoding": "application/json", 46 + "schema": { 47 + "type": "object", 48 + "required": [ 49 + "token", 50 + "invite" 51 + ], 52 + "properties": { 53 + "token": { 54 + "type": "string", 55 + "description": "Raw token. Shown once — cannot be retrieved later." 56 + }, 57 + "invite": { 58 + "type": "ref", 59 + "ref": "rsvp.atmo.space.defs#inviteView" 60 + } 61 + } 62 + } 63 + }, 64 + "errors": [ 65 + { 66 + "name": "NotFound" 67 + }, 68 + { 69 + "name": "Forbidden" 70 + } 71 + ] 72 + } 73 + } 74 + }
+52
lexicons-generated/rsvp/atmo/space/invite/list.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.invite.list", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List invites for a space. Owner only.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri" 12 + ], 13 + "properties": { 14 + "spaceUri": { 15 + "type": "string", 16 + "format": "at-uri" 17 + }, 18 + "includeRevoked": { 19 + "type": "boolean", 20 + "default": false 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "object", 28 + "required": [ 29 + "invites" 30 + ], 31 + "properties": { 32 + "invites": { 33 + "type": "array", 34 + "items": { 35 + "type": "ref", 36 + "ref": "rsvp.atmo.space.defs#inviteView" 37 + } 38 + } 39 + } 40 + } 41 + }, 42 + "errors": [ 43 + { 44 + "name": "NotFound" 45 + }, 46 + { 47 + "name": "Forbidden" 48 + } 49 + ] 50 + } 51 + } 52 + }
+52
lexicons-generated/rsvp/atmo/space/invite/redeem.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.invite.redeem", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Redeem an invite token. The JWT issuer becomes a member of the space with the invite's perms.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "token" 14 + ], 15 + "properties": { 16 + "token": { 17 + "type": "string" 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": [ 27 + "spaceUri", 28 + "perms" 29 + ], 30 + "properties": { 31 + "spaceUri": { 32 + "type": "string", 33 + "format": "at-uri" 34 + }, 35 + "perms": { 36 + "type": "string", 37 + "knownValues": [ 38 + "read", 39 + "write" 40 + ] 41 + } 42 + } 43 + } 44 + }, 45 + "errors": [ 46 + { 47 + "name": "InvalidInvite" 48 + } 49 + ] 50 + } 51 + } 52 + }
+51
lexicons-generated/rsvp/atmo/space/invite/revoke.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.invite.revoke", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Revoke an invite. Owner only. Invites are identified by their tokenHash (visible in list output).", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "tokenHash" 15 + ], 16 + "properties": { 17 + "spaceUri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "tokenHash": { 22 + "type": "string" 23 + } 24 + } 25 + } 26 + }, 27 + "output": { 28 + "encoding": "application/json", 29 + "schema": { 30 + "type": "object", 31 + "required": [ 32 + "ok" 33 + ], 34 + "properties": { 35 + "ok": { 36 + "type": "boolean" 37 + } 38 + } 39 + } 40 + }, 41 + "errors": [ 42 + { 43 + "name": "NotFound" 44 + }, 45 + { 46 + "name": "Forbidden" 47 + } 48 + ] 49 + } 50 + } 51 + }
+48
lexicons-generated/rsvp/atmo/space/listMembers.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.listMembers", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List members of a space. Caller must be a member or the owner.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri" 12 + ], 13 + "properties": { 14 + "spaceUri": { 15 + "type": "string", 16 + "format": "at-uri" 17 + } 18 + } 19 + }, 20 + "output": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "required": [ 25 + "members" 26 + ], 27 + "properties": { 28 + "members": { 29 + "type": "array", 30 + "items": { 31 + "type": "ref", 32 + "ref": "rsvp.atmo.space.defs#memberView" 33 + } 34 + } 35 + } 36 + } 37 + }, 38 + "errors": [ 39 + { 40 + "name": "NotFound" 41 + }, 42 + { 43 + "name": "Forbidden" 44 + } 45 + ] 46 + } 47 + } 48 + }
+70
lexicons-generated/rsvp/atmo/space/listRecords.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.listRecords", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List records of a given collection within a space. Access is governed by the space's collection policy.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "spaceUri", 12 + "collection" 13 + ], 14 + "properties": { 15 + "spaceUri": { 16 + "type": "string", 17 + "format": "at-uri" 18 + }, 19 + "collection": { 20 + "type": "string", 21 + "format": "nsid" 22 + }, 23 + "byUser": { 24 + "type": "string", 25 + "format": "did", 26 + "description": "Only return records authored by this DID." 27 + }, 28 + "cursor": { 29 + "type": "string" 30 + }, 31 + "limit": { 32 + "type": "integer", 33 + "minimum": 1, 34 + "maximum": 200, 35 + "default": 50 36 + } 37 + } 38 + }, 39 + "output": { 40 + "encoding": "application/json", 41 + "schema": { 42 + "type": "object", 43 + "required": [ 44 + "records" 45 + ], 46 + "properties": { 47 + "records": { 48 + "type": "array", 49 + "items": { 50 + "type": "ref", 51 + "ref": "rsvp.atmo.space.defs#recordView" 52 + } 53 + }, 54 + "cursor": { 55 + "type": "string" 56 + } 57 + } 58 + } 59 + }, 60 + "errors": [ 61 + { 62 + "name": "NotFound" 63 + }, 64 + { 65 + "name": "Forbidden" 66 + } 67 + ] 68 + } 69 + } 70 + }
+57
lexicons-generated/rsvp/atmo/space/listSpaces.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.listSpaces", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List spaces the caller has access to. Default scope is 'member' (spaces the caller is a member of, including owned); 'owner' lists only spaces the caller owns.", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "scope": { 12 + "type": "string", 13 + "knownValues": [ 14 + "member", 15 + "owner" 16 + ], 17 + "default": "member" 18 + }, 19 + "type": { 20 + "type": "string", 21 + "format": "nsid" 22 + }, 23 + "cursor": { 24 + "type": "string" 25 + }, 26 + "limit": { 27 + "type": "integer", 28 + "minimum": 1, 29 + "maximum": 200, 30 + "default": 50 31 + } 32 + } 33 + }, 34 + "output": { 35 + "encoding": "application/json", 36 + "schema": { 37 + "type": "object", 38 + "required": [ 39 + "spaces" 40 + ], 41 + "properties": { 42 + "spaces": { 43 + "type": "array", 44 + "items": { 45 + "type": "ref", 46 + "ref": "rsvp.atmo.space.defs#spaceView" 47 + } 48 + }, 49 + "cursor": { 50 + "type": "string" 51 + } 52 + } 53 + } 54 + } 55 + } 56 + } 57 + }
+68
lexicons-generated/rsvp/atmo/space/putRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "rsvp.atmo.space.putRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Write a record into a space. The author is always the JWT issuer. If rkey is omitted, a TID is generated.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "spaceUri", 14 + "collection", 15 + "record" 16 + ], 17 + "properties": { 18 + "spaceUri": { 19 + "type": "string", 20 + "format": "at-uri" 21 + }, 22 + "collection": { 23 + "type": "string", 24 + "format": "nsid" 25 + }, 26 + "rkey": { 27 + "type": "string" 28 + }, 29 + "record": { 30 + "type": "unknown" 31 + } 32 + } 33 + } 34 + }, 35 + "output": { 36 + "encoding": "application/json", 37 + "schema": { 38 + "type": "object", 39 + "required": [ 40 + "rkey", 41 + "authorDid", 42 + "createdAt" 43 + ], 44 + "properties": { 45 + "rkey": { 46 + "type": "string" 47 + }, 48 + "authorDid": { 49 + "type": "string", 50 + "format": "did" 51 + }, 52 + "createdAt": { 53 + "type": "integer" 54 + } 55 + } 56 + } 57 + }, 58 + "errors": [ 59 + { 60 + "name": "NotFound" 61 + }, 62 + { 63 + "name": "Forbidden" 64 + } 65 + ] 66 + } 67 + } 68 + }
+2 -1
package.json
··· 26 26 "vods:download": "tsx scripts/vod-processing/download-audio.ts", 27 27 "vods:transcribe": "tsx scripts/vod-processing/transcribe.ts", 28 28 "vods:summarize": "tsx scripts/vod-processing/summarize.ts", 29 - "vods:vtt": "tsx scripts/vod-processing/generate-vtt.ts" 29 + "vods:vtt": "tsx scripts/vod-processing/generate-vtt.ts", 30 + "publish-lexicons": "tsx --env-file=.env scripts/publish-lexicons.ts" 30 31 }, 31 32 "devDependencies": { 32 33 "@atcute/atproto": "^3.1.10",
+40
scripts/publish-lexicons.ts
··· 1 + /** 2 + * Publish atmo-events' generated lexicons to the specified account's PDS. 3 + * 4 + * Usage: 5 + * LEXICON_ACCOUNT_IDENTIFIER=you.bsky.social \ 6 + * LEXICON_ACCOUNT_PASSWORD=xxxx-xxxx-xxxx-xxxx \ 7 + * pnpm publish-lexicons 8 + * 9 + * pnpm publish-lexicons <identifier> <app-password> 10 + */ 11 + 12 + import { join, dirname } from 'node:path'; 13 + import { fileURLToPath } from 'node:url'; 14 + import { publishLexicons } from '@atmo-dev/contrail/publish'; 15 + 16 + const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); 17 + 18 + async function main(): Promise<void> { 19 + const identifier = process.argv[2] ?? process.env.LEXICON_ACCOUNT_IDENTIFIER; 20 + const password = process.argv[3] ?? process.env.LEXICON_ACCOUNT_PASSWORD; 21 + 22 + if (!identifier || !password) { 23 + console.error( 24 + 'Usage: pnpm publish-lexicons <handle-or-did> <app-password>\n' + 25 + ' (or set LEXICON_ACCOUNT_IDENTIFIER and LEXICON_ACCOUNT_PASSWORD env vars)\n' 26 + ); 27 + process.exit(1); 28 + } 29 + 30 + await publishLexicons({ 31 + generatedDir: join(ROOT, 'lexicons-generated'), 32 + identifier, 33 + password 34 + }); 35 + } 36 + 37 + main().catch((err) => { 38 + console.error(err); 39 + process.exit(1); 40 + });
+19 -4
src/lexicon-types/index.ts
··· 1 1 export * as AppBskyActorProfile from "./types/app/bsky/actor/profile.js"; 2 2 export * as CommunityLexiconCalendarEvent from "./types/community/lexicon/calendar/event.js"; 3 - export * as CommunityLexiconCalendarEventGetRecord from "./types/community/lexicon/calendar/event/getRecord.js"; 4 - export * as CommunityLexiconCalendarEventListRecords from "./types/community/lexicon/calendar/event/listRecords.js"; 5 3 export * as CommunityLexiconCalendarRsvp from "./types/community/lexicon/calendar/rsvp.js"; 6 - export * as CommunityLexiconCalendarRsvpGetRecord from "./types/community/lexicon/calendar/rsvp/getRecord.js"; 7 - export * as CommunityLexiconCalendarRsvpListRecords from "./types/community/lexicon/calendar/rsvp/listRecords.js"; 8 4 export * as CommunityLexiconLocationAddress from "./types/community/lexicon/location/address.js"; 9 5 export * as CommunityLexiconLocationFsq from "./types/community/lexicon/location/fsq.js"; 10 6 export * as CommunityLexiconLocationGeo from "./types/community/lexicon/location/geo.js"; 11 7 export * as CommunityLexiconLocationHthree from "./types/community/lexicon/location/hthree.js"; 8 + export * as RsvpAtmoEventGetRecord from "./types/rsvp/atmo/event/getRecord.js"; 9 + export * as RsvpAtmoEventListRecords from "./types/rsvp/atmo/event/listRecords.js"; 12 10 export * as RsvpAtmoGetCursor from "./types/rsvp/atmo/getCursor.js"; 13 11 export * as RsvpAtmoGetOverview from "./types/rsvp/atmo/getOverview.js"; 14 12 export * as RsvpAtmoGetProfile from "./types/rsvp/atmo/getProfile.js"; 15 13 export * as RsvpAtmoNotifyOfUpdate from "./types/rsvp/atmo/notifyOfUpdate.js"; 14 + export * as RsvpAtmoRsvpGetRecord from "./types/rsvp/atmo/rsvp/getRecord.js"; 15 + export * as RsvpAtmoRsvpListRecords from "./types/rsvp/atmo/rsvp/listRecords.js"; 16 + export * as RsvpAtmoSpaceAdminAddMember from "./types/rsvp/atmo/space/admin/addMember.js"; 17 + export * as RsvpAtmoSpaceAdminCreateSpace from "./types/rsvp/atmo/space/admin/createSpace.js"; 18 + export * as RsvpAtmoSpaceAdminRemoveMember from "./types/rsvp/atmo/space/admin/removeMember.js"; 19 + export * as RsvpAtmoSpaceDefs from "./types/rsvp/atmo/space/defs.js"; 20 + export * as RsvpAtmoSpaceDeleteRecord from "./types/rsvp/atmo/space/deleteRecord.js"; 21 + export * as RsvpAtmoSpaceGetRecord from "./types/rsvp/atmo/space/getRecord.js"; 22 + export * as RsvpAtmoSpaceGetSpace from "./types/rsvp/atmo/space/getSpace.js"; 23 + export * as RsvpAtmoSpaceInviteCreate from "./types/rsvp/atmo/space/invite/create.js"; 24 + export * as RsvpAtmoSpaceInviteList from "./types/rsvp/atmo/space/invite/list.js"; 25 + export * as RsvpAtmoSpaceInviteRedeem from "./types/rsvp/atmo/space/invite/redeem.js"; 26 + export * as RsvpAtmoSpaceInviteRevoke from "./types/rsvp/atmo/space/invite/revoke.js"; 27 + export * as RsvpAtmoSpaceListMembers from "./types/rsvp/atmo/space/listMembers.js"; 28 + export * as RsvpAtmoSpaceListRecords from "./types/rsvp/atmo/space/listRecords.js"; 29 + export * as RsvpAtmoSpaceListSpaces from "./types/rsvp/atmo/space/listSpaces.js"; 30 + export * as RsvpAtmoSpacePutRecord from "./types/rsvp/atmo/space/putRecord.js";
+238
src/lexicon-types/types/rsvp/atmo/event/getRecord.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as ComAtprotoLabelDefs from "@atcute/atproto/types/label/defs"; 5 + import * as ComAtprotoRepoStrongRef from "@atcute/atproto/types/repo/strongRef"; 6 + import * as CommunityLexiconCalendarEvent from "../../../community/lexicon/calendar/event.js"; 7 + import * as CommunityLexiconCalendarRsvp from "../../../community/lexicon/calendar/rsvp.js"; 8 + 9 + const _appBskyActorProfileSchema = /*#__PURE__*/ v.object({ 10 + $type: /*#__PURE__*/ v.optional( 11 + /*#__PURE__*/ v.literal("rsvp.atmo.event.getRecord#appBskyActorProfile"), 12 + ), 13 + /** 14 + * Small image to be displayed next to posts from account. AKA, 'profile picture' 15 + * @accept image/png, image/jpeg 16 + * @maxSize 1000000 17 + */ 18 + avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.blob()), 19 + /** 20 + * Larger horizontal image to display behind profile view. 21 + * @accept image/png, image/jpeg 22 + * @maxSize 1000000 23 + */ 24 + banner: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.blob()), 25 + createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 26 + /** 27 + * Free-form profile description text. 28 + * @maxLength 2560 29 + * @maxGraphemes 256 30 + */ 31 + description: /*#__PURE__*/ v.optional( 32 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 33 + /*#__PURE__*/ v.stringLength(0, 2560), 34 + /*#__PURE__*/ v.stringGraphemes(0, 256), 35 + ]), 36 + ), 37 + /** 38 + * @maxLength 640 39 + * @maxGraphemes 64 40 + */ 41 + displayName: /*#__PURE__*/ v.optional( 42 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 43 + /*#__PURE__*/ v.stringLength(0, 640), 44 + /*#__PURE__*/ v.stringGraphemes(0, 64), 45 + ]), 46 + ), 47 + get joinedViaStarterPack() { 48 + return /*#__PURE__*/ v.optional(ComAtprotoRepoStrongRef.mainSchema); 49 + }, 50 + /** 51 + * Self-label values, specific to the Bluesky application, on the overall account. 52 + */ 53 + get labels() { 54 + return /*#__PURE__*/ v.optional( 55 + /*#__PURE__*/ v.variant([ComAtprotoLabelDefs.selfLabelsSchema]), 56 + ); 57 + }, 58 + get pinnedPost() { 59 + return /*#__PURE__*/ v.optional(ComAtprotoRepoStrongRef.mainSchema); 60 + }, 61 + /** 62 + * Free-form pronouns text. 63 + * @maxLength 200 64 + * @maxGraphemes 20 65 + */ 66 + pronouns: /*#__PURE__*/ v.optional( 67 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 68 + /*#__PURE__*/ v.stringLength(0, 200), 69 + /*#__PURE__*/ v.stringGraphemes(0, 20), 70 + ]), 71 + ), 72 + website: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 73 + }); 74 + const _hydrateRsvpsSchema = /*#__PURE__*/ v.object({ 75 + $type: /*#__PURE__*/ v.optional( 76 + /*#__PURE__*/ v.literal("rsvp.atmo.event.getRecord#hydrateRsvps"), 77 + ), 78 + get going() { 79 + return /*#__PURE__*/ v.optional( 80 + /*#__PURE__*/ v.array(hydrateRsvpsRecordSchema), 81 + ); 82 + }, 83 + get interested() { 84 + return /*#__PURE__*/ v.optional( 85 + /*#__PURE__*/ v.array(hydrateRsvpsRecordSchema), 86 + ); 87 + }, 88 + get notgoing() { 89 + return /*#__PURE__*/ v.optional( 90 + /*#__PURE__*/ v.array(hydrateRsvpsRecordSchema), 91 + ); 92 + }, 93 + get other() { 94 + return /*#__PURE__*/ v.optional( 95 + /*#__PURE__*/ v.array(hydrateRsvpsRecordSchema), 96 + ); 97 + }, 98 + }); 99 + const _hydrateRsvpsRecordSchema = /*#__PURE__*/ v.object({ 100 + $type: /*#__PURE__*/ v.optional( 101 + /*#__PURE__*/ v.literal("rsvp.atmo.event.getRecord#hydrateRsvpsRecord"), 102 + ), 103 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 104 + collection: /*#__PURE__*/ v.nsidString(), 105 + did: /*#__PURE__*/ v.didString(), 106 + get record() { 107 + return /*#__PURE__*/ v.optional(CommunityLexiconCalendarRsvp.mainSchema); 108 + }, 109 + rkey: /*#__PURE__*/ v.string(), 110 + /** 111 + * Present when the record was read from a permissioned space. 112 + */ 113 + space: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 114 + time_us: /*#__PURE__*/ v.integer(), 115 + uri: /*#__PURE__*/ v.resourceUriString(), 116 + }); 117 + const _mainSchema = /*#__PURE__*/ v.query("rsvp.atmo.event.getRecord", { 118 + params: /*#__PURE__*/ v.object({ 119 + /** 120 + * Number of rsvps records to embed 121 + * @minimum 1 122 + * @maximum 50 123 + */ 124 + hydrateRsvps: /*#__PURE__*/ v.optional( 125 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [ 126 + /*#__PURE__*/ v.integerRange(1, 50), 127 + ]), 128 + ), 129 + /** 130 + * Include profile + identity info keyed by DID 131 + */ 132 + profiles: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 133 + /** 134 + * If set, fetch from this permissioned space (requires service-auth JWT). 135 + */ 136 + spaceUri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 137 + /** 138 + * AT URI of the record 139 + */ 140 + uri: /*#__PURE__*/ v.resourceUriString(), 141 + }), 142 + output: { 143 + type: "lex", 144 + schema: /*#__PURE__*/ v.object({ 145 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 146 + collection: /*#__PURE__*/ v.nsidString(), 147 + did: /*#__PURE__*/ v.didString(), 148 + get profiles() { 149 + return /*#__PURE__*/ v.optional( 150 + /*#__PURE__*/ v.array(profileEntrySchema), 151 + ); 152 + }, 153 + get record() { 154 + return /*#__PURE__*/ v.optional( 155 + CommunityLexiconCalendarEvent.mainSchema, 156 + ); 157 + }, 158 + rkey: /*#__PURE__*/ v.string(), 159 + get rsvps() { 160 + return /*#__PURE__*/ v.optional(hydrateRsvpsSchema); 161 + }, 162 + /** 163 + * Total rsvps count 164 + */ 165 + rsvpsCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 166 + /** 167 + * rsvps count where status = going 168 + */ 169 + rsvpsGoingCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 170 + /** 171 + * rsvps count where status = interested 172 + */ 173 + rsvpsInterestedCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 174 + /** 175 + * rsvps count where status = notgoing 176 + */ 177 + rsvpsNotgoingCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 178 + /** 179 + * Present when the record was read from a permissioned space; its value is the space URI. 180 + */ 181 + space: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 182 + time_us: /*#__PURE__*/ v.integer(), 183 + uri: /*#__PURE__*/ v.resourceUriString(), 184 + }), 185 + }, 186 + }); 187 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 188 + $type: /*#__PURE__*/ v.optional( 189 + /*#__PURE__*/ v.literal("rsvp.atmo.event.getRecord#profileEntry"), 190 + ), 191 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 192 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 193 + did: /*#__PURE__*/ v.didString(), 194 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 195 + get record() { 196 + return /*#__PURE__*/ v.optional(appBskyActorProfileSchema); 197 + }, 198 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 199 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 200 + }); 201 + 202 + type appBskyActorProfile$schematype = typeof _appBskyActorProfileSchema; 203 + type hydrateRsvps$schematype = typeof _hydrateRsvpsSchema; 204 + type hydrateRsvpsRecord$schematype = typeof _hydrateRsvpsRecordSchema; 205 + type main$schematype = typeof _mainSchema; 206 + type profileEntry$schematype = typeof _profileEntrySchema; 207 + 208 + export interface appBskyActorProfileSchema extends appBskyActorProfile$schematype {} 209 + export interface hydrateRsvpsSchema extends hydrateRsvps$schematype {} 210 + export interface hydrateRsvpsRecordSchema extends hydrateRsvpsRecord$schematype {} 211 + export interface mainSchema extends main$schematype {} 212 + export interface profileEntrySchema extends profileEntry$schematype {} 213 + 214 + export const appBskyActorProfileSchema = 215 + _appBskyActorProfileSchema as appBskyActorProfileSchema; 216 + export const hydrateRsvpsSchema = _hydrateRsvpsSchema as hydrateRsvpsSchema; 217 + export const hydrateRsvpsRecordSchema = 218 + _hydrateRsvpsRecordSchema as hydrateRsvpsRecordSchema; 219 + export const mainSchema = _mainSchema as mainSchema; 220 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 221 + 222 + export interface AppBskyActorProfile extends v.InferInput< 223 + typeof appBskyActorProfileSchema 224 + > {} 225 + export interface HydrateRsvps extends v.InferInput<typeof hydrateRsvpsSchema> {} 226 + export interface HydrateRsvpsRecord extends v.InferInput< 227 + typeof hydrateRsvpsRecordSchema 228 + > {} 229 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 230 + 231 + export interface $params extends v.InferInput<mainSchema["params"]> {} 232 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 233 + 234 + declare module "@atcute/lexicons/ambient" { 235 + interface XRPCQueries { 236 + "rsvp.atmo.event.getRecord": mainSchema; 237 + } 238 + }
+352
src/lexicon-types/types/rsvp/atmo/event/listRecords.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as ComAtprotoLabelDefs from "@atcute/atproto/types/label/defs"; 5 + import * as ComAtprotoRepoStrongRef from "@atcute/atproto/types/repo/strongRef"; 6 + import * as CommunityLexiconCalendarEvent from "../../../community/lexicon/calendar/event.js"; 7 + import * as CommunityLexiconCalendarRsvp from "../../../community/lexicon/calendar/rsvp.js"; 8 + 9 + const _appBskyActorProfileSchema = /*#__PURE__*/ v.object({ 10 + $type: /*#__PURE__*/ v.optional( 11 + /*#__PURE__*/ v.literal("rsvp.atmo.event.listRecords#appBskyActorProfile"), 12 + ), 13 + /** 14 + * Small image to be displayed next to posts from account. AKA, 'profile picture' 15 + * @accept image/png, image/jpeg 16 + * @maxSize 1000000 17 + */ 18 + avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.blob()), 19 + /** 20 + * Larger horizontal image to display behind profile view. 21 + * @accept image/png, image/jpeg 22 + * @maxSize 1000000 23 + */ 24 + banner: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.blob()), 25 + createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 26 + /** 27 + * Free-form profile description text. 28 + * @maxLength 2560 29 + * @maxGraphemes 256 30 + */ 31 + description: /*#__PURE__*/ v.optional( 32 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 33 + /*#__PURE__*/ v.stringLength(0, 2560), 34 + /*#__PURE__*/ v.stringGraphemes(0, 256), 35 + ]), 36 + ), 37 + /** 38 + * @maxLength 640 39 + * @maxGraphemes 64 40 + */ 41 + displayName: /*#__PURE__*/ v.optional( 42 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 43 + /*#__PURE__*/ v.stringLength(0, 640), 44 + /*#__PURE__*/ v.stringGraphemes(0, 64), 45 + ]), 46 + ), 47 + get joinedViaStarterPack() { 48 + return /*#__PURE__*/ v.optional(ComAtprotoRepoStrongRef.mainSchema); 49 + }, 50 + /** 51 + * Self-label values, specific to the Bluesky application, on the overall account. 52 + */ 53 + get labels() { 54 + return /*#__PURE__*/ v.optional( 55 + /*#__PURE__*/ v.variant([ComAtprotoLabelDefs.selfLabelsSchema]), 56 + ); 57 + }, 58 + get pinnedPost() { 59 + return /*#__PURE__*/ v.optional(ComAtprotoRepoStrongRef.mainSchema); 60 + }, 61 + /** 62 + * Free-form pronouns text. 63 + * @maxLength 200 64 + * @maxGraphemes 20 65 + */ 66 + pronouns: /*#__PURE__*/ v.optional( 67 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 68 + /*#__PURE__*/ v.stringLength(0, 200), 69 + /*#__PURE__*/ v.stringGraphemes(0, 20), 70 + ]), 71 + ), 72 + website: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 73 + }); 74 + const _hydrateRsvpsSchema = /*#__PURE__*/ v.object({ 75 + $type: /*#__PURE__*/ v.optional( 76 + /*#__PURE__*/ v.literal("rsvp.atmo.event.listRecords#hydrateRsvps"), 77 + ), 78 + get going() { 79 + return /*#__PURE__*/ v.optional( 80 + /*#__PURE__*/ v.array(hydrateRsvpsRecordSchema), 81 + ); 82 + }, 83 + get interested() { 84 + return /*#__PURE__*/ v.optional( 85 + /*#__PURE__*/ v.array(hydrateRsvpsRecordSchema), 86 + ); 87 + }, 88 + get notgoing() { 89 + return /*#__PURE__*/ v.optional( 90 + /*#__PURE__*/ v.array(hydrateRsvpsRecordSchema), 91 + ); 92 + }, 93 + get other() { 94 + return /*#__PURE__*/ v.optional( 95 + /*#__PURE__*/ v.array(hydrateRsvpsRecordSchema), 96 + ); 97 + }, 98 + }); 99 + const _hydrateRsvpsRecordSchema = /*#__PURE__*/ v.object({ 100 + $type: /*#__PURE__*/ v.optional( 101 + /*#__PURE__*/ v.literal("rsvp.atmo.event.listRecords#hydrateRsvpsRecord"), 102 + ), 103 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 104 + collection: /*#__PURE__*/ v.nsidString(), 105 + did: /*#__PURE__*/ v.didString(), 106 + get record() { 107 + return /*#__PURE__*/ v.optional(CommunityLexiconCalendarRsvp.mainSchema); 108 + }, 109 + rkey: /*#__PURE__*/ v.string(), 110 + /** 111 + * Present when the record was read from a permissioned space. 112 + */ 113 + space: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 114 + time_us: /*#__PURE__*/ v.integer(), 115 + uri: /*#__PURE__*/ v.resourceUriString(), 116 + }); 117 + const _mainSchema = /*#__PURE__*/ v.query("rsvp.atmo.event.listRecords", { 118 + params: /*#__PURE__*/ v.object({ 119 + /** 120 + * Filter by DID or handle (triggers on-demand backfill) 121 + */ 122 + actor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.actorIdentifierString()), 123 + /** 124 + * Only used with spaceUri — filter to records authored by this DID. 125 + */ 126 + byUser: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.didString()), 127 + /** 128 + * Maximum value for createdAt 129 + */ 130 + createdAtMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 131 + /** 132 + * Minimum value for createdAt 133 + */ 134 + createdAtMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 135 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 136 + /** 137 + * Filter by description 138 + */ 139 + description: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 140 + /** 141 + * Maximum value for endsAt 142 + */ 143 + endsAtMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 144 + /** 145 + * Minimum value for endsAt 146 + */ 147 + endsAtMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 148 + /** 149 + * Number of rsvps records to embed per record 150 + * @minimum 1 151 + * @maximum 50 152 + */ 153 + hydrateRsvps: /*#__PURE__*/ v.optional( 154 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [ 155 + /*#__PURE__*/ v.integerRange(1, 50), 156 + ]), 157 + ), 158 + /** 159 + * @minimum 1 160 + * @maximum 200 161 + * @default 50 162 + */ 163 + limit: /*#__PURE__*/ v.optional( 164 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [ 165 + /*#__PURE__*/ v.integerRange(1, 200), 166 + ]), 167 + 50, 168 + ), 169 + /** 170 + * Filter by mode 171 + */ 172 + mode: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 173 + /** 174 + * Filter by name 175 + */ 176 + name: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 177 + /** 178 + * Sort direction (default: desc for dates/numbers/counts, asc for strings) 179 + */ 180 + order: /*#__PURE__*/ v.optional( 181 + /*#__PURE__*/ v.string<"asc" | "desc" | (string & {})>(), 182 + ), 183 + /** 184 + * Include profile + identity info keyed by DID 185 + */ 186 + profiles: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 187 + /** 188 + * Minimum total rsvps count 189 + */ 190 + rsvpsCountMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 191 + /** 192 + * Minimum rsvps count where status = going 193 + */ 194 + rsvpsGoingCountMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 195 + /** 196 + * Minimum rsvps count where status = interested 197 + */ 198 + rsvpsInterestedCountMin: /*#__PURE__*/ v.optional( 199 + /*#__PURE__*/ v.integer(), 200 + ), 201 + /** 202 + * Minimum rsvps count where status = notgoing 203 + */ 204 + rsvpsNotgoingCountMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 205 + /** 206 + * Full-text search across: mode, name, status, description 207 + */ 208 + search: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 209 + /** 210 + * Field to sort by (default: time_us) 211 + */ 212 + sort: /*#__PURE__*/ v.optional( 213 + /*#__PURE__*/ v.string< 214 + | "createdAt" 215 + | "description" 216 + | "endsAt" 217 + | "mode" 218 + | "name" 219 + | "rsvpsCount" 220 + | "rsvpsGoingCount" 221 + | "rsvpsInterestedCount" 222 + | "rsvpsNotgoingCount" 223 + | "startsAt" 224 + | "status" 225 + | (string & {}) 226 + >(), 227 + ), 228 + /** 229 + * If set, query records inside this permissioned space (requires service-auth JWT). 230 + */ 231 + spaceUri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 232 + /** 233 + * Maximum value for startsAt 234 + */ 235 + startsAtMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 236 + /** 237 + * Minimum value for startsAt 238 + */ 239 + startsAtMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 240 + /** 241 + * Filter by status 242 + */ 243 + status: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 244 + }), 245 + output: { 246 + type: "lex", 247 + schema: /*#__PURE__*/ v.object({ 248 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 249 + get profiles() { 250 + return /*#__PURE__*/ v.optional( 251 + /*#__PURE__*/ v.array(profileEntrySchema), 252 + ); 253 + }, 254 + get records() { 255 + return /*#__PURE__*/ v.array(recordSchema); 256 + }, 257 + }), 258 + }, 259 + }); 260 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 261 + $type: /*#__PURE__*/ v.optional( 262 + /*#__PURE__*/ v.literal("rsvp.atmo.event.listRecords#profileEntry"), 263 + ), 264 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 265 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 266 + did: /*#__PURE__*/ v.didString(), 267 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 268 + get record() { 269 + return /*#__PURE__*/ v.optional(appBskyActorProfileSchema); 270 + }, 271 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 272 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 273 + }); 274 + const _recordSchema = /*#__PURE__*/ v.object({ 275 + $type: /*#__PURE__*/ v.optional( 276 + /*#__PURE__*/ v.literal("rsvp.atmo.event.listRecords#record"), 277 + ), 278 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 279 + collection: /*#__PURE__*/ v.nsidString(), 280 + did: /*#__PURE__*/ v.didString(), 281 + get record() { 282 + return /*#__PURE__*/ v.optional(CommunityLexiconCalendarEvent.mainSchema); 283 + }, 284 + rkey: /*#__PURE__*/ v.string(), 285 + get rsvps() { 286 + return /*#__PURE__*/ v.optional(hydrateRsvpsSchema); 287 + }, 288 + /** 289 + * Total rsvps count 290 + */ 291 + rsvpsCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 292 + /** 293 + * rsvps count where status = going 294 + */ 295 + rsvpsGoingCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 296 + /** 297 + * rsvps count where status = interested 298 + */ 299 + rsvpsInterestedCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 300 + /** 301 + * rsvps count where status = notgoing 302 + */ 303 + rsvpsNotgoingCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 304 + /** 305 + * Present when the record was read from a permissioned space; its value is the space URI. 306 + */ 307 + space: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 308 + time_us: /*#__PURE__*/ v.integer(), 309 + uri: /*#__PURE__*/ v.resourceUriString(), 310 + }); 311 + 312 + type appBskyActorProfile$schematype = typeof _appBskyActorProfileSchema; 313 + type hydrateRsvps$schematype = typeof _hydrateRsvpsSchema; 314 + type hydrateRsvpsRecord$schematype = typeof _hydrateRsvpsRecordSchema; 315 + type main$schematype = typeof _mainSchema; 316 + type profileEntry$schematype = typeof _profileEntrySchema; 317 + type record$schematype = typeof _recordSchema; 318 + 319 + export interface appBskyActorProfileSchema extends appBskyActorProfile$schematype {} 320 + export interface hydrateRsvpsSchema extends hydrateRsvps$schematype {} 321 + export interface hydrateRsvpsRecordSchema extends hydrateRsvpsRecord$schematype {} 322 + export interface mainSchema extends main$schematype {} 323 + export interface profileEntrySchema extends profileEntry$schematype {} 324 + export interface recordSchema extends record$schematype {} 325 + 326 + export const appBskyActorProfileSchema = 327 + _appBskyActorProfileSchema as appBskyActorProfileSchema; 328 + export const hydrateRsvpsSchema = _hydrateRsvpsSchema as hydrateRsvpsSchema; 329 + export const hydrateRsvpsRecordSchema = 330 + _hydrateRsvpsRecordSchema as hydrateRsvpsRecordSchema; 331 + export const mainSchema = _mainSchema as mainSchema; 332 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 333 + export const recordSchema = _recordSchema as recordSchema; 334 + 335 + export interface AppBskyActorProfile extends v.InferInput< 336 + typeof appBskyActorProfileSchema 337 + > {} 338 + export interface HydrateRsvps extends v.InferInput<typeof hydrateRsvpsSchema> {} 339 + export interface HydrateRsvpsRecord extends v.InferInput< 340 + typeof hydrateRsvpsRecordSchema 341 + > {} 342 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 343 + export interface Record extends v.InferInput<typeof recordSchema> {} 344 + 345 + export interface $params extends v.InferInput<mainSchema["params"]> {} 346 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 347 + 348 + declare module "@atcute/lexicons/ambient" { 349 + interface XRPCQueries { 350 + "rsvp.atmo.event.listRecords": mainSchema; 351 + } 352 + }
+6 -4
src/lexicon-types/types/rsvp/atmo/getProfile.ts
··· 78 78 }), 79 79 output: { 80 80 type: "lex", 81 - get schema() { 82 - return profileEntrySchema; 83 - }, 81 + schema: /*#__PURE__*/ v.object({ 82 + get profiles() { 83 + return /*#__PURE__*/ v.array(profileEntrySchema); 84 + }, 85 + }), 84 86 }, 85 87 }); 86 88 const _profileEntrySchema = /*#__PURE__*/ v.object({ ··· 117 119 export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 118 120 119 121 export interface $params extends v.InferInput<mainSchema["params"]> {} 120 - export type $output = v.InferXRPCBodyInput<mainSchema["output"]>; 122 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 121 123 122 124 declare module "@atcute/lexicons/ambient" { 123 125 interface XRPCQueries {
+187
src/lexicon-types/types/rsvp/atmo/rsvp/getRecord.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as ComAtprotoLabelDefs from "@atcute/atproto/types/label/defs"; 5 + import * as ComAtprotoRepoStrongRef from "@atcute/atproto/types/repo/strongRef"; 6 + import * as CommunityLexiconCalendarEvent from "../../../community/lexicon/calendar/event.js"; 7 + import * as CommunityLexiconCalendarRsvp from "../../../community/lexicon/calendar/rsvp.js"; 8 + 9 + const _appBskyActorProfileSchema = /*#__PURE__*/ v.object({ 10 + $type: /*#__PURE__*/ v.optional( 11 + /*#__PURE__*/ v.literal("rsvp.atmo.rsvp.getRecord#appBskyActorProfile"), 12 + ), 13 + /** 14 + * Small image to be displayed next to posts from account. AKA, 'profile picture' 15 + * @accept image/png, image/jpeg 16 + * @maxSize 1000000 17 + */ 18 + avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.blob()), 19 + /** 20 + * Larger horizontal image to display behind profile view. 21 + * @accept image/png, image/jpeg 22 + * @maxSize 1000000 23 + */ 24 + banner: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.blob()), 25 + createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 26 + /** 27 + * Free-form profile description text. 28 + * @maxLength 2560 29 + * @maxGraphemes 256 30 + */ 31 + description: /*#__PURE__*/ v.optional( 32 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 33 + /*#__PURE__*/ v.stringLength(0, 2560), 34 + /*#__PURE__*/ v.stringGraphemes(0, 256), 35 + ]), 36 + ), 37 + /** 38 + * @maxLength 640 39 + * @maxGraphemes 64 40 + */ 41 + displayName: /*#__PURE__*/ v.optional( 42 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 43 + /*#__PURE__*/ v.stringLength(0, 640), 44 + /*#__PURE__*/ v.stringGraphemes(0, 64), 45 + ]), 46 + ), 47 + get joinedViaStarterPack() { 48 + return /*#__PURE__*/ v.optional(ComAtprotoRepoStrongRef.mainSchema); 49 + }, 50 + /** 51 + * Self-label values, specific to the Bluesky application, on the overall account. 52 + */ 53 + get labels() { 54 + return /*#__PURE__*/ v.optional( 55 + /*#__PURE__*/ v.variant([ComAtprotoLabelDefs.selfLabelsSchema]), 56 + ); 57 + }, 58 + get pinnedPost() { 59 + return /*#__PURE__*/ v.optional(ComAtprotoRepoStrongRef.mainSchema); 60 + }, 61 + /** 62 + * Free-form pronouns text. 63 + * @maxLength 200 64 + * @maxGraphemes 20 65 + */ 66 + pronouns: /*#__PURE__*/ v.optional( 67 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 68 + /*#__PURE__*/ v.stringLength(0, 200), 69 + /*#__PURE__*/ v.stringGraphemes(0, 20), 70 + ]), 71 + ), 72 + website: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 73 + }); 74 + const _mainSchema = /*#__PURE__*/ v.query("rsvp.atmo.rsvp.getRecord", { 75 + params: /*#__PURE__*/ v.object({ 76 + /** 77 + * Embed the referenced event record 78 + */ 79 + hydrateEvent: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 80 + /** 81 + * Include profile + identity info keyed by DID 82 + */ 83 + profiles: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 84 + /** 85 + * If set, fetch from this permissioned space (requires service-auth JWT). 86 + */ 87 + spaceUri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 88 + /** 89 + * AT URI of the record 90 + */ 91 + uri: /*#__PURE__*/ v.resourceUriString(), 92 + }), 93 + output: { 94 + type: "lex", 95 + schema: /*#__PURE__*/ v.object({ 96 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 97 + collection: /*#__PURE__*/ v.nsidString(), 98 + did: /*#__PURE__*/ v.didString(), 99 + get event() { 100 + return /*#__PURE__*/ v.optional(refEventRecordSchema); 101 + }, 102 + get profiles() { 103 + return /*#__PURE__*/ v.optional( 104 + /*#__PURE__*/ v.array(profileEntrySchema), 105 + ); 106 + }, 107 + get record() { 108 + return /*#__PURE__*/ v.optional( 109 + CommunityLexiconCalendarRsvp.mainSchema, 110 + ); 111 + }, 112 + rkey: /*#__PURE__*/ v.string(), 113 + /** 114 + * Present when the record was read from a permissioned space; its value is the space URI. 115 + */ 116 + space: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 117 + time_us: /*#__PURE__*/ v.integer(), 118 + uri: /*#__PURE__*/ v.resourceUriString(), 119 + }), 120 + }, 121 + }); 122 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 123 + $type: /*#__PURE__*/ v.optional( 124 + /*#__PURE__*/ v.literal("rsvp.atmo.rsvp.getRecord#profileEntry"), 125 + ), 126 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 127 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 128 + did: /*#__PURE__*/ v.didString(), 129 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 130 + get record() { 131 + return /*#__PURE__*/ v.optional(appBskyActorProfileSchema); 132 + }, 133 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 134 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 135 + }); 136 + const _refEventRecordSchema = /*#__PURE__*/ v.object({ 137 + $type: /*#__PURE__*/ v.optional( 138 + /*#__PURE__*/ v.literal("rsvp.atmo.rsvp.getRecord#refEventRecord"), 139 + ), 140 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 141 + collection: /*#__PURE__*/ v.nsidString(), 142 + did: /*#__PURE__*/ v.didString(), 143 + get record() { 144 + return /*#__PURE__*/ v.optional(CommunityLexiconCalendarEvent.mainSchema); 145 + }, 146 + rkey: /*#__PURE__*/ v.string(), 147 + /** 148 + * Present when the record was read from a permissioned space. 149 + */ 150 + space: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 151 + time_us: /*#__PURE__*/ v.integer(), 152 + uri: /*#__PURE__*/ v.resourceUriString(), 153 + }); 154 + 155 + type appBskyActorProfile$schematype = typeof _appBskyActorProfileSchema; 156 + type main$schematype = typeof _mainSchema; 157 + type profileEntry$schematype = typeof _profileEntrySchema; 158 + type refEventRecord$schematype = typeof _refEventRecordSchema; 159 + 160 + export interface appBskyActorProfileSchema extends appBskyActorProfile$schematype {} 161 + export interface mainSchema extends main$schematype {} 162 + export interface profileEntrySchema extends profileEntry$schematype {} 163 + export interface refEventRecordSchema extends refEventRecord$schematype {} 164 + 165 + export const appBskyActorProfileSchema = 166 + _appBskyActorProfileSchema as appBskyActorProfileSchema; 167 + export const mainSchema = _mainSchema as mainSchema; 168 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 169 + export const refEventRecordSchema = 170 + _refEventRecordSchema as refEventRecordSchema; 171 + 172 + export interface AppBskyActorProfile extends v.InferInput< 173 + typeof appBskyActorProfileSchema 174 + > {} 175 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 176 + export interface RefEventRecord extends v.InferInput< 177 + typeof refEventRecordSchema 178 + > {} 179 + 180 + export interface $params extends v.InferInput<mainSchema["params"]> {} 181 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 182 + 183 + declare module "@atcute/lexicons/ambient" { 184 + interface XRPCQueries { 185 + "rsvp.atmo.rsvp.getRecord": mainSchema; 186 + } 187 + }
+234
src/lexicon-types/types/rsvp/atmo/rsvp/listRecords.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as ComAtprotoLabelDefs from "@atcute/atproto/types/label/defs"; 5 + import * as ComAtprotoRepoStrongRef from "@atcute/atproto/types/repo/strongRef"; 6 + import * as CommunityLexiconCalendarEvent from "../../../community/lexicon/calendar/event.js"; 7 + import * as CommunityLexiconCalendarRsvp from "../../../community/lexicon/calendar/rsvp.js"; 8 + 9 + const _appBskyActorProfileSchema = /*#__PURE__*/ v.object({ 10 + $type: /*#__PURE__*/ v.optional( 11 + /*#__PURE__*/ v.literal("rsvp.atmo.rsvp.listRecords#appBskyActorProfile"), 12 + ), 13 + /** 14 + * Small image to be displayed next to posts from account. AKA, 'profile picture' 15 + * @accept image/png, image/jpeg 16 + * @maxSize 1000000 17 + */ 18 + avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.blob()), 19 + /** 20 + * Larger horizontal image to display behind profile view. 21 + * @accept image/png, image/jpeg 22 + * @maxSize 1000000 23 + */ 24 + banner: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.blob()), 25 + createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 26 + /** 27 + * Free-form profile description text. 28 + * @maxLength 2560 29 + * @maxGraphemes 256 30 + */ 31 + description: /*#__PURE__*/ v.optional( 32 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 33 + /*#__PURE__*/ v.stringLength(0, 2560), 34 + /*#__PURE__*/ v.stringGraphemes(0, 256), 35 + ]), 36 + ), 37 + /** 38 + * @maxLength 640 39 + * @maxGraphemes 64 40 + */ 41 + displayName: /*#__PURE__*/ v.optional( 42 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 43 + /*#__PURE__*/ v.stringLength(0, 640), 44 + /*#__PURE__*/ v.stringGraphemes(0, 64), 45 + ]), 46 + ), 47 + get joinedViaStarterPack() { 48 + return /*#__PURE__*/ v.optional(ComAtprotoRepoStrongRef.mainSchema); 49 + }, 50 + /** 51 + * Self-label values, specific to the Bluesky application, on the overall account. 52 + */ 53 + get labels() { 54 + return /*#__PURE__*/ v.optional( 55 + /*#__PURE__*/ v.variant([ComAtprotoLabelDefs.selfLabelsSchema]), 56 + ); 57 + }, 58 + get pinnedPost() { 59 + return /*#__PURE__*/ v.optional(ComAtprotoRepoStrongRef.mainSchema); 60 + }, 61 + /** 62 + * Free-form pronouns text. 63 + * @maxLength 200 64 + * @maxGraphemes 20 65 + */ 66 + pronouns: /*#__PURE__*/ v.optional( 67 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 68 + /*#__PURE__*/ v.stringLength(0, 200), 69 + /*#__PURE__*/ v.stringGraphemes(0, 20), 70 + ]), 71 + ), 72 + website: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 73 + }); 74 + const _mainSchema = /*#__PURE__*/ v.query("rsvp.atmo.rsvp.listRecords", { 75 + params: /*#__PURE__*/ v.object({ 76 + /** 77 + * Filter by DID or handle (triggers on-demand backfill) 78 + */ 79 + actor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.actorIdentifierString()), 80 + /** 81 + * Only used with spaceUri — filter to records authored by this DID. 82 + */ 83 + byUser: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.didString()), 84 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 85 + /** 86 + * Embed the referenced event record 87 + */ 88 + hydrateEvent: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 89 + /** 90 + * @minimum 1 91 + * @maximum 200 92 + * @default 50 93 + */ 94 + limit: /*#__PURE__*/ v.optional( 95 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [ 96 + /*#__PURE__*/ v.integerRange(1, 200), 97 + ]), 98 + 50, 99 + ), 100 + /** 101 + * Sort direction (default: desc for dates/numbers/counts, asc for strings) 102 + */ 103 + order: /*#__PURE__*/ v.optional( 104 + /*#__PURE__*/ v.string<"asc" | "desc" | (string & {})>(), 105 + ), 106 + /** 107 + * Include profile + identity info keyed by DID 108 + */ 109 + profiles: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 110 + /** 111 + * Field to sort by (default: time_us) 112 + */ 113 + sort: /*#__PURE__*/ v.optional( 114 + /*#__PURE__*/ v.string<"status" | "subjectUri" | (string & {})>(), 115 + ), 116 + /** 117 + * If set, query records inside this permissioned space (requires service-auth JWT). 118 + */ 119 + spaceUri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 120 + /** 121 + * Filter by status 122 + */ 123 + status: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 124 + /** 125 + * Filter by subject.uri 126 + */ 127 + subjectUri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 128 + }), 129 + output: { 130 + type: "lex", 131 + schema: /*#__PURE__*/ v.object({ 132 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 133 + get profiles() { 134 + return /*#__PURE__*/ v.optional( 135 + /*#__PURE__*/ v.array(profileEntrySchema), 136 + ); 137 + }, 138 + get records() { 139 + return /*#__PURE__*/ v.array(recordSchema); 140 + }, 141 + }), 142 + }, 143 + }); 144 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 145 + $type: /*#__PURE__*/ v.optional( 146 + /*#__PURE__*/ v.literal("rsvp.atmo.rsvp.listRecords#profileEntry"), 147 + ), 148 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 149 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 150 + did: /*#__PURE__*/ v.didString(), 151 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 152 + get record() { 153 + return /*#__PURE__*/ v.optional(appBskyActorProfileSchema); 154 + }, 155 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 156 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 157 + }); 158 + const _recordSchema = /*#__PURE__*/ v.object({ 159 + $type: /*#__PURE__*/ v.optional( 160 + /*#__PURE__*/ v.literal("rsvp.atmo.rsvp.listRecords#record"), 161 + ), 162 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 163 + collection: /*#__PURE__*/ v.nsidString(), 164 + did: /*#__PURE__*/ v.didString(), 165 + get event() { 166 + return /*#__PURE__*/ v.optional(refEventRecordSchema); 167 + }, 168 + get record() { 169 + return /*#__PURE__*/ v.optional(CommunityLexiconCalendarRsvp.mainSchema); 170 + }, 171 + rkey: /*#__PURE__*/ v.string(), 172 + /** 173 + * Present when the record was read from a permissioned space; its value is the space URI. 174 + */ 175 + space: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 176 + time_us: /*#__PURE__*/ v.integer(), 177 + uri: /*#__PURE__*/ v.resourceUriString(), 178 + }); 179 + const _refEventRecordSchema = /*#__PURE__*/ v.object({ 180 + $type: /*#__PURE__*/ v.optional( 181 + /*#__PURE__*/ v.literal("rsvp.atmo.rsvp.listRecords#refEventRecord"), 182 + ), 183 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 184 + collection: /*#__PURE__*/ v.nsidString(), 185 + did: /*#__PURE__*/ v.didString(), 186 + get record() { 187 + return /*#__PURE__*/ v.optional(CommunityLexiconCalendarEvent.mainSchema); 188 + }, 189 + rkey: /*#__PURE__*/ v.string(), 190 + /** 191 + * Present when the record was read from a permissioned space. 192 + */ 193 + space: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 194 + time_us: /*#__PURE__*/ v.integer(), 195 + uri: /*#__PURE__*/ v.resourceUriString(), 196 + }); 197 + 198 + type appBskyActorProfile$schematype = typeof _appBskyActorProfileSchema; 199 + type main$schematype = typeof _mainSchema; 200 + type profileEntry$schematype = typeof _profileEntrySchema; 201 + type record$schematype = typeof _recordSchema; 202 + type refEventRecord$schematype = typeof _refEventRecordSchema; 203 + 204 + export interface appBskyActorProfileSchema extends appBskyActorProfile$schematype {} 205 + export interface mainSchema extends main$schematype {} 206 + export interface profileEntrySchema extends profileEntry$schematype {} 207 + export interface recordSchema extends record$schematype {} 208 + export interface refEventRecordSchema extends refEventRecord$schematype {} 209 + 210 + export const appBskyActorProfileSchema = 211 + _appBskyActorProfileSchema as appBskyActorProfileSchema; 212 + export const mainSchema = _mainSchema as mainSchema; 213 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 214 + export const recordSchema = _recordSchema as recordSchema; 215 + export const refEventRecordSchema = 216 + _refEventRecordSchema as refEventRecordSchema; 217 + 218 + export interface AppBskyActorProfile extends v.InferInput< 219 + typeof appBskyActorProfileSchema 220 + > {} 221 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 222 + export interface Record extends v.InferInput<typeof recordSchema> {} 223 + export interface RefEventRecord extends v.InferInput< 224 + typeof refEventRecordSchema 225 + > {} 226 + 227 + export interface $params extends v.InferInput<mainSchema["params"]> {} 228 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 229 + 230 + declare module "@atcute/lexicons/ambient" { 231 + interface XRPCQueries { 232 + "rsvp.atmo.rsvp.listRecords": mainSchema; 233 + } 234 + }
+46
src/lexicon-types/types/rsvp/atmo/space/admin/addMember.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.procedure( 6 + "rsvp.atmo.space.admin.addMember", 7 + { 8 + params: null, 9 + input: { 10 + type: "lex", 11 + schema: /*#__PURE__*/ v.object({ 12 + did: /*#__PURE__*/ v.didString(), 13 + /** 14 + * @default "write" 15 + */ 16 + perms: /*#__PURE__*/ v.optional( 17 + /*#__PURE__*/ v.string<"read" | "write" | (string & {})>(), 18 + "write", 19 + ), 20 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 21 + }), 22 + }, 23 + output: { 24 + type: "lex", 25 + schema: /*#__PURE__*/ v.object({ 26 + ok: /*#__PURE__*/ v.boolean(), 27 + }), 28 + }, 29 + }, 30 + ); 31 + 32 + type main$schematype = typeof _mainSchema; 33 + 34 + export interface mainSchema extends main$schematype {} 35 + 36 + export const mainSchema = _mainSchema as mainSchema; 37 + 38 + export interface $params {} 39 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 40 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 41 + 42 + declare module "@atcute/lexicons/ambient" { 43 + interface XRPCProcedures { 44 + "rsvp.atmo.space.admin.addMember": mainSchema; 45 + } 46 + }
+57
src/lexicon-types/types/rsvp/atmo/space/admin/createSpace.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as RsvpAtmoSpaceDefs from "../defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.procedure( 7 + "rsvp.atmo.space.admin.createSpace", 8 + { 9 + params: null, 10 + input: { 11 + type: "lex", 12 + schema: /*#__PURE__*/ v.object({ 13 + get appPolicy() { 14 + return /*#__PURE__*/ v.optional(RsvpAtmoSpaceDefs.appPolicySchema); 15 + }, 16 + appPolicyRef: /*#__PURE__*/ v.optional( 17 + /*#__PURE__*/ v.resourceUriString(), 18 + ), 19 + /** 20 + * Space key. Auto-generated (TID) if omitted. 21 + */ 22 + key: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 23 + memberListRef: /*#__PURE__*/ v.optional( 24 + /*#__PURE__*/ v.resourceUriString(), 25 + ), 26 + /** 27 + * Space type NSID. Defaults to the service's configured type. 28 + */ 29 + type: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 30 + }), 31 + }, 32 + output: { 33 + type: "lex", 34 + schema: /*#__PURE__*/ v.object({ 35 + get space() { 36 + return RsvpAtmoSpaceDefs.spaceViewSchema; 37 + }, 38 + }), 39 + }, 40 + }, 41 + ); 42 + 43 + type main$schematype = typeof _mainSchema; 44 + 45 + export interface mainSchema extends main$schematype {} 46 + 47 + export const mainSchema = _mainSchema as mainSchema; 48 + 49 + export interface $params {} 50 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 51 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 52 + 53 + declare module "@atcute/lexicons/ambient" { 54 + interface XRPCProcedures { 55 + "rsvp.atmo.space.admin.createSpace": mainSchema; 56 + } 57 + }
+39
src/lexicon-types/types/rsvp/atmo/space/admin/removeMember.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.procedure( 6 + "rsvp.atmo.space.admin.removeMember", 7 + { 8 + params: null, 9 + input: { 10 + type: "lex", 11 + schema: /*#__PURE__*/ v.object({ 12 + did: /*#__PURE__*/ v.didString(), 13 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 14 + }), 15 + }, 16 + output: { 17 + type: "lex", 18 + schema: /*#__PURE__*/ v.object({ 19 + ok: /*#__PURE__*/ v.boolean(), 20 + }), 21 + }, 22 + }, 23 + ); 24 + 25 + type main$schematype = typeof _mainSchema; 26 + 27 + export interface mainSchema extends main$schematype {} 28 + 29 + export const mainSchema = _mainSchema as mainSchema; 30 + 31 + export interface $params {} 32 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 33 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 34 + 35 + declare module "@atcute/lexicons/ambient" { 36 + interface XRPCProcedures { 37 + "rsvp.atmo.space.admin.removeMember": mainSchema; 38 + } 39 + }
+95
src/lexicon-types/types/rsvp/atmo/space/defs.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + 4 + const _appPolicySchema = /*#__PURE__*/ v.object({ 5 + $type: /*#__PURE__*/ v.optional( 6 + /*#__PURE__*/ v.literal("rsvp.atmo.space.defs#appPolicy"), 7 + ), 8 + apps: /*#__PURE__*/ v.array(/*#__PURE__*/ v.string()), 9 + /** 10 + * 'allow' = default-allow with apps[] as denylist; 'deny' = default-deny with apps[] as allowlist. 11 + */ 12 + mode: /*#__PURE__*/ v.string<"allow" | "deny" | (string & {})>(), 13 + }); 14 + const _inviteViewSchema = /*#__PURE__*/ v.object({ 15 + $type: /*#__PURE__*/ v.optional( 16 + /*#__PURE__*/ v.literal("rsvp.atmo.space.defs#inviteView"), 17 + ), 18 + createdAt: /*#__PURE__*/ v.integer(), 19 + createdBy: /*#__PURE__*/ v.didString(), 20 + expiresAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 21 + maxUses: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 22 + note: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 23 + perms: /*#__PURE__*/ v.string<"read" | "write" | (string & {})>(), 24 + revokedAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 25 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 26 + tokenHash: /*#__PURE__*/ v.string(), 27 + usedCount: /*#__PURE__*/ v.integer(), 28 + }); 29 + const _memberViewSchema = /*#__PURE__*/ v.object({ 30 + $type: /*#__PURE__*/ v.optional( 31 + /*#__PURE__*/ v.literal("rsvp.atmo.space.defs#memberView"), 32 + ), 33 + addedAt: /*#__PURE__*/ v.integer(), 34 + addedBy: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.didString()), 35 + did: /*#__PURE__*/ v.didString(), 36 + /** 37 + * 'write' implies 'read'. Space owner is always implicit write. 38 + */ 39 + perms: /*#__PURE__*/ v.string<"read" | "write" | (string & {})>(), 40 + }); 41 + const _recordViewSchema = /*#__PURE__*/ v.object({ 42 + $type: /*#__PURE__*/ v.optional( 43 + /*#__PURE__*/ v.literal("rsvp.atmo.space.defs#recordView"), 44 + ), 45 + authorDid: /*#__PURE__*/ v.didString(), 46 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.cidString()), 47 + collection: /*#__PURE__*/ v.nsidString(), 48 + createdAt: /*#__PURE__*/ v.integer(), 49 + record: /*#__PURE__*/ v.unknown(), 50 + rkey: /*#__PURE__*/ v.string(), 51 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 52 + }); 53 + const _spaceViewSchema = /*#__PURE__*/ v.object({ 54 + $type: /*#__PURE__*/ v.optional( 55 + /*#__PURE__*/ v.literal("rsvp.atmo.space.defs#spaceView"), 56 + ), 57 + /** 58 + * Owner-only 59 + */ 60 + get appPolicy() { 61 + return /*#__PURE__*/ v.optional(appPolicySchema); 62 + }, 63 + appPolicyRef: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 64 + createdAt: /*#__PURE__*/ v.integer(), 65 + key: /*#__PURE__*/ v.string(), 66 + memberListRef: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 67 + ownerDid: /*#__PURE__*/ v.didString(), 68 + serviceDid: /*#__PURE__*/ v.string(), 69 + type: /*#__PURE__*/ v.nsidString(), 70 + uri: /*#__PURE__*/ v.resourceUriString(), 71 + }); 72 + 73 + type appPolicy$schematype = typeof _appPolicySchema; 74 + type inviteView$schematype = typeof _inviteViewSchema; 75 + type memberView$schematype = typeof _memberViewSchema; 76 + type recordView$schematype = typeof _recordViewSchema; 77 + type spaceView$schematype = typeof _spaceViewSchema; 78 + 79 + export interface appPolicySchema extends appPolicy$schematype {} 80 + export interface inviteViewSchema extends inviteView$schematype {} 81 + export interface memberViewSchema extends memberView$schematype {} 82 + export interface recordViewSchema extends recordView$schematype {} 83 + export interface spaceViewSchema extends spaceView$schematype {} 84 + 85 + export const appPolicySchema = _appPolicySchema as appPolicySchema; 86 + export const inviteViewSchema = _inviteViewSchema as inviteViewSchema; 87 + export const memberViewSchema = _memberViewSchema as memberViewSchema; 88 + export const recordViewSchema = _recordViewSchema as recordViewSchema; 89 + export const spaceViewSchema = _spaceViewSchema as spaceViewSchema; 90 + 91 + export interface AppPolicy extends v.InferInput<typeof appPolicySchema> {} 92 + export interface InviteView extends v.InferInput<typeof inviteViewSchema> {} 93 + export interface MemberView extends v.InferInput<typeof memberViewSchema> {} 94 + export interface RecordView extends v.InferInput<typeof recordViewSchema> {} 95 + export interface SpaceView extends v.InferInput<typeof spaceViewSchema> {}
+37
src/lexicon-types/types/rsvp/atmo/space/deleteRecord.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.procedure("rsvp.atmo.space.deleteRecord", { 6 + params: null, 7 + input: { 8 + type: "lex", 9 + schema: /*#__PURE__*/ v.object({ 10 + collection: /*#__PURE__*/ v.nsidString(), 11 + rkey: /*#__PURE__*/ v.string(), 12 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 13 + }), 14 + }, 15 + output: { 16 + type: "lex", 17 + schema: /*#__PURE__*/ v.object({ 18 + ok: /*#__PURE__*/ v.boolean(), 19 + }), 20 + }, 21 + }); 22 + 23 + type main$schematype = typeof _mainSchema; 24 + 25 + export interface mainSchema extends main$schematype {} 26 + 27 + export const mainSchema = _mainSchema as mainSchema; 28 + 29 + export interface $params {} 30 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 31 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 32 + 33 + declare module "@atcute/lexicons/ambient" { 34 + interface XRPCProcedures { 35 + "rsvp.atmo.space.deleteRecord": mainSchema; 36 + } 37 + }
+36
src/lexicon-types/types/rsvp/atmo/space/getRecord.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as RsvpAtmoSpaceDefs from "./defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query("rsvp.atmo.space.getRecord", { 7 + params: /*#__PURE__*/ v.object({ 8 + author: /*#__PURE__*/ v.didString(), 9 + collection: /*#__PURE__*/ v.nsidString(), 10 + rkey: /*#__PURE__*/ v.string(), 11 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 12 + }), 13 + output: { 14 + type: "lex", 15 + schema: /*#__PURE__*/ v.object({ 16 + get record() { 17 + return RsvpAtmoSpaceDefs.recordViewSchema; 18 + }, 19 + }), 20 + }, 21 + }); 22 + 23 + type main$schematype = typeof _mainSchema; 24 + 25 + export interface mainSchema extends main$schematype {} 26 + 27 + export const mainSchema = _mainSchema as mainSchema; 28 + 29 + export interface $params extends v.InferInput<mainSchema["params"]> {} 30 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 31 + 32 + declare module "@atcute/lexicons/ambient" { 33 + interface XRPCQueries { 34 + "rsvp.atmo.space.getRecord": mainSchema; 35 + } 36 + }
+33
src/lexicon-types/types/rsvp/atmo/space/getSpace.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as RsvpAtmoSpaceDefs from "./defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query("rsvp.atmo.space.getSpace", { 7 + params: /*#__PURE__*/ v.object({ 8 + uri: /*#__PURE__*/ v.resourceUriString(), 9 + }), 10 + output: { 11 + type: "lex", 12 + schema: /*#__PURE__*/ v.object({ 13 + get space() { 14 + return RsvpAtmoSpaceDefs.spaceViewSchema; 15 + }, 16 + }), 17 + }, 18 + }); 19 + 20 + type main$schematype = typeof _mainSchema; 21 + 22 + export interface mainSchema extends main$schematype {} 23 + 24 + export const mainSchema = _mainSchema as mainSchema; 25 + 26 + export interface $params extends v.InferInput<mainSchema["params"]> {} 27 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 28 + 29 + declare module "@atcute/lexicons/ambient" { 30 + interface XRPCQueries { 31 + "rsvp.atmo.space.getSpace": mainSchema; 32 + } 33 + }
+70
src/lexicon-types/types/rsvp/atmo/space/invite/create.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as RsvpAtmoSpaceDefs from "../defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.procedure("rsvp.atmo.space.invite.create", { 7 + params: null, 8 + input: { 9 + type: "lex", 10 + schema: /*#__PURE__*/ v.object({ 11 + /** 12 + * Unix ms timestamp. Omit for no expiry. 13 + */ 14 + expiresAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 15 + /** 16 + * Omit for unlimited uses. 17 + * @minimum 1 18 + */ 19 + maxUses: /*#__PURE__*/ v.optional( 20 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [ 21 + /*#__PURE__*/ v.integerRange(1), 22 + ]), 23 + ), 24 + /** 25 + * @maxLength 500 26 + */ 27 + note: /*#__PURE__*/ v.optional( 28 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 29 + /*#__PURE__*/ v.stringLength(0, 500), 30 + ]), 31 + ), 32 + /** 33 + * @default "write" 34 + */ 35 + perms: /*#__PURE__*/ v.optional( 36 + /*#__PURE__*/ v.string<"read" | "write" | (string & {})>(), 37 + "write", 38 + ), 39 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 40 + }), 41 + }, 42 + output: { 43 + type: "lex", 44 + schema: /*#__PURE__*/ v.object({ 45 + get invite() { 46 + return RsvpAtmoSpaceDefs.inviteViewSchema; 47 + }, 48 + /** 49 + * Raw token. Shown once — cannot be retrieved later. 50 + */ 51 + token: /*#__PURE__*/ v.string(), 52 + }), 53 + }, 54 + }); 55 + 56 + type main$schematype = typeof _mainSchema; 57 + 58 + export interface mainSchema extends main$schematype {} 59 + 60 + export const mainSchema = _mainSchema as mainSchema; 61 + 62 + export interface $params {} 63 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 64 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 65 + 66 + declare module "@atcute/lexicons/ambient" { 67 + interface XRPCProcedures { 68 + "rsvp.atmo.space.invite.create": mainSchema; 69 + } 70 + }
+37
src/lexicon-types/types/rsvp/atmo/space/invite/list.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as RsvpAtmoSpaceDefs from "../defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query("rsvp.atmo.space.invite.list", { 7 + params: /*#__PURE__*/ v.object({ 8 + /** 9 + * @default false 10 + */ 11 + includeRevoked: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean(), false), 12 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 13 + }), 14 + output: { 15 + type: "lex", 16 + schema: /*#__PURE__*/ v.object({ 17 + get invites() { 18 + return /*#__PURE__*/ v.array(RsvpAtmoSpaceDefs.inviteViewSchema); 19 + }, 20 + }), 21 + }, 22 + }); 23 + 24 + type main$schematype = typeof _mainSchema; 25 + 26 + export interface mainSchema extends main$schematype {} 27 + 28 + export const mainSchema = _mainSchema as mainSchema; 29 + 30 + export interface $params extends v.InferInput<mainSchema["params"]> {} 31 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 32 + 33 + declare module "@atcute/lexicons/ambient" { 34 + interface XRPCQueries { 35 + "rsvp.atmo.space.invite.list": mainSchema; 36 + } 37 + }
+36
src/lexicon-types/types/rsvp/atmo/space/invite/redeem.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.procedure("rsvp.atmo.space.invite.redeem", { 6 + params: null, 7 + input: { 8 + type: "lex", 9 + schema: /*#__PURE__*/ v.object({ 10 + token: /*#__PURE__*/ v.string(), 11 + }), 12 + }, 13 + output: { 14 + type: "lex", 15 + schema: /*#__PURE__*/ v.object({ 16 + perms: /*#__PURE__*/ v.string<"read" | "write" | (string & {})>(), 17 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 18 + }), 19 + }, 20 + }); 21 + 22 + type main$schematype = typeof _mainSchema; 23 + 24 + export interface mainSchema extends main$schematype {} 25 + 26 + export const mainSchema = _mainSchema as mainSchema; 27 + 28 + export interface $params {} 29 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 30 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 31 + 32 + declare module "@atcute/lexicons/ambient" { 33 + interface XRPCProcedures { 34 + "rsvp.atmo.space.invite.redeem": mainSchema; 35 + } 36 + }
+36
src/lexicon-types/types/rsvp/atmo/space/invite/revoke.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.procedure("rsvp.atmo.space.invite.revoke", { 6 + params: null, 7 + input: { 8 + type: "lex", 9 + schema: /*#__PURE__*/ v.object({ 10 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 11 + tokenHash: /*#__PURE__*/ v.string(), 12 + }), 13 + }, 14 + output: { 15 + type: "lex", 16 + schema: /*#__PURE__*/ v.object({ 17 + ok: /*#__PURE__*/ v.boolean(), 18 + }), 19 + }, 20 + }); 21 + 22 + type main$schematype = typeof _mainSchema; 23 + 24 + export interface mainSchema extends main$schematype {} 25 + 26 + export const mainSchema = _mainSchema as mainSchema; 27 + 28 + export interface $params {} 29 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 30 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 31 + 32 + declare module "@atcute/lexicons/ambient" { 33 + interface XRPCProcedures { 34 + "rsvp.atmo.space.invite.revoke": mainSchema; 35 + } 36 + }
+33
src/lexicon-types/types/rsvp/atmo/space/listMembers.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as RsvpAtmoSpaceDefs from "./defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query("rsvp.atmo.space.listMembers", { 7 + params: /*#__PURE__*/ v.object({ 8 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 9 + }), 10 + output: { 11 + type: "lex", 12 + schema: /*#__PURE__*/ v.object({ 13 + get members() { 14 + return /*#__PURE__*/ v.array(RsvpAtmoSpaceDefs.memberViewSchema); 15 + }, 16 + }), 17 + }, 18 + }); 19 + 20 + type main$schematype = typeof _mainSchema; 21 + 22 + export interface mainSchema extends main$schematype {} 23 + 24 + export const mainSchema = _mainSchema as mainSchema; 25 + 26 + export interface $params extends v.InferInput<mainSchema["params"]> {} 27 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 28 + 29 + declare module "@atcute/lexicons/ambient" { 30 + interface XRPCQueries { 31 + "rsvp.atmo.space.listMembers": mainSchema; 32 + } 33 + }
+51
src/lexicon-types/types/rsvp/atmo/space/listRecords.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as RsvpAtmoSpaceDefs from "./defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query("rsvp.atmo.space.listRecords", { 7 + params: /*#__PURE__*/ v.object({ 8 + /** 9 + * Only return records authored by this DID. 10 + */ 11 + byUser: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.didString()), 12 + collection: /*#__PURE__*/ v.nsidString(), 13 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 14 + /** 15 + * @minimum 1 16 + * @maximum 200 17 + * @default 50 18 + */ 19 + limit: /*#__PURE__*/ v.optional( 20 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [ 21 + /*#__PURE__*/ v.integerRange(1, 200), 22 + ]), 23 + 50, 24 + ), 25 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 26 + }), 27 + output: { 28 + type: "lex", 29 + schema: /*#__PURE__*/ v.object({ 30 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 31 + get records() { 32 + return /*#__PURE__*/ v.array(RsvpAtmoSpaceDefs.recordViewSchema); 33 + }, 34 + }), 35 + }, 36 + }); 37 + 38 + type main$schematype = typeof _mainSchema; 39 + 40 + export interface mainSchema extends main$schematype {} 41 + 42 + export const mainSchema = _mainSchema as mainSchema; 43 + 44 + export interface $params extends v.InferInput<mainSchema["params"]> {} 45 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 46 + 47 + declare module "@atcute/lexicons/ambient" { 48 + interface XRPCQueries { 49 + "rsvp.atmo.space.listRecords": mainSchema; 50 + } 51 + }
+53
src/lexicon-types/types/rsvp/atmo/space/listSpaces.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + import * as RsvpAtmoSpaceDefs from "./defs.js"; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query("rsvp.atmo.space.listSpaces", { 7 + params: /*#__PURE__*/ v.object({ 8 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 9 + /** 10 + * @minimum 1 11 + * @maximum 200 12 + * @default 50 13 + */ 14 + limit: /*#__PURE__*/ v.optional( 15 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [ 16 + /*#__PURE__*/ v.integerRange(1, 200), 17 + ]), 18 + 50, 19 + ), 20 + /** 21 + * @default "member" 22 + */ 23 + scope: /*#__PURE__*/ v.optional( 24 + /*#__PURE__*/ v.string<"member" | "owner" | (string & {})>(), 25 + "member", 26 + ), 27 + type: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 28 + }), 29 + output: { 30 + type: "lex", 31 + schema: /*#__PURE__*/ v.object({ 32 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 33 + get spaces() { 34 + return /*#__PURE__*/ v.array(RsvpAtmoSpaceDefs.spaceViewSchema); 35 + }, 36 + }), 37 + }, 38 + }); 39 + 40 + type main$schematype = typeof _mainSchema; 41 + 42 + export interface mainSchema extends main$schematype {} 43 + 44 + export const mainSchema = _mainSchema as mainSchema; 45 + 46 + export interface $params extends v.InferInput<mainSchema["params"]> {} 47 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 48 + 49 + declare module "@atcute/lexicons/ambient" { 50 + interface XRPCQueries { 51 + "rsvp.atmo.space.listSpaces": mainSchema; 52 + } 53 + }
+40
src/lexicon-types/types/rsvp/atmo/space/putRecord.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.procedure("rsvp.atmo.space.putRecord", { 6 + params: null, 7 + input: { 8 + type: "lex", 9 + schema: /*#__PURE__*/ v.object({ 10 + collection: /*#__PURE__*/ v.nsidString(), 11 + record: /*#__PURE__*/ v.unknown(), 12 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 13 + spaceUri: /*#__PURE__*/ v.resourceUriString(), 14 + }), 15 + }, 16 + output: { 17 + type: "lex", 18 + schema: /*#__PURE__*/ v.object({ 19 + authorDid: /*#__PURE__*/ v.didString(), 20 + createdAt: /*#__PURE__*/ v.integer(), 21 + rkey: /*#__PURE__*/ v.string(), 22 + }), 23 + }, 24 + }); 25 + 26 + type main$schematype = typeof _mainSchema; 27 + 28 + export interface mainSchema extends main$schematype {} 29 + 30 + export const mainSchema = _mainSchema as mainSchema; 31 + 32 + export interface $params {} 33 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 34 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 35 + 36 + declare module "@atcute/lexicons/ambient" { 37 + interface XRPCProcedures { 38 + "rsvp.atmo.space.putRecord": mainSchema; 39 + } 40 + }
+5 -1
src/lib/atproto/scripts/tunnel.ts
··· 159 159 } 160 160 161 161 function writeGeneratedService(hostname: string, tunnelUrl: string): void { 162 - const did = `did:web:${hostname}#${SERVICE_FRAGMENT}`; 162 + // NOTE: plain DID, no fragment. PDSes reject fragments in getServiceAuth's `aud` param, 163 + // and the library's middleware does strict string equality. The fragment is only used 164 + // by PDSes to look up service entries in the DID doc for Atproto-Proxy routing — that 165 + // concern is separate from JWT audience validation. 166 + const did = `did:web:${hostname}`; 163 167 const body = 164 168 `/** Auto-generated by \`pnpm tunnel\`. Do not edit by hand.\n` + 165 169 ` * When the tunnel is running, this file is rewritten with the tunnel's\n` +
+10 -18
src/lib/atproto/settings.ts
··· 1 1 import { dev } from '$app/environment'; 2 2 import { scope } from '@atcute/oauth-node-client'; 3 3 4 - // writable collections 4 + // writable collections — declared as a standalone scope because their NSIDs 5 + // (`community.lexicon.*`) sit outside our namespace, so they can't go in 6 + // `rsvp.atmo.permissionSet` (permission sets can only reference NSIDs in their 7 + // own namespace). 5 8 export const collections = [ 6 9 'community.lexicon.calendar.event', 7 10 'community.lexicon.calendar.rsvp' ··· 9 12 10 13 export type AllowedCollection = (typeof collections)[number]; 11 14 12 - /** Permissioned-spaces XRPC methods the app needs to call on the user's behalf. 13 - * `aud: '*'` lets one consent cover dev (tunnel DID) and prod (published DID) without re-consenting. */ 14 - const spaceMethods = [ 15 - 'tools.atmo.space.admin.createSpace', 16 - 'tools.atmo.space.admin.addMember', 17 - 'tools.atmo.space.putRecord', 18 - 'tools.atmo.space.listRecords', 19 - 'tools.atmo.space.getRecord', 20 - 'tools.atmo.space.getSpace', 21 - 'tools.atmo.space.invite.create', 22 - 'tools.atmo.space.invite.redeem', 23 - 'tools.atmo.space.invite.list', 24 - 'tools.atmo.space.invite.revoke' 25 - ] as const; 26 - 27 - // OAuth scope — add scope.blob(), scope.rpc(), etc. as needed 15 + // OAuth scopes. `include:rsvp.atmo.permissionSet?aud=*` bundles every rpc method 16 + // the deployment exposes; `aud=*` lets the same consent cover dev (tunnel DID) 17 + // and prod (published DID) without re-consenting. Repo writes and blob uploads 18 + // live as standalone scopes since they reference NSIDs (or resource kinds) 19 + // outside the `rsvp.atmo` namespace. 28 20 export const scopes = [ 29 21 'atproto', 30 22 scope.repo({ collection: [...collections] }), 31 23 scope.blob({ accept: ['image/*'] }), 32 - ...spaceMethods.map((lxm) => scope.rpc({ lxm: [lxm], aud: '*' })) 24 + 'include:rsvp.atmo.permissionSet' 33 25 ]; 34 26 35 27 // set to false to disable signup
+19 -4
src/lib/components/EventCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { getCDNImageBlobUrl } from '$lib/atproto'; 3 - import { isEventOngoing, type FlatEventRecord } from '$lib/contrail'; 3 + import { eventUrl, isEventOngoing, type FlatEventRecord } from '$lib/contrail'; 4 4 import Avatar from 'svelte-boring-avatars'; 5 5 6 6 let { ··· 63 63 </script> 64 64 65 65 <a 66 - href="/p/{actor || event.did}/e/{event.rkey}" 66 + href={eventUrl(event, actor)} 67 67 class="group grid grid-cols-[4rem_1fr] gap-3 transition-colors sm:grid-cols-[5rem_1fr] sm:gap-4" 68 68 > 69 69 <div class="w-full"> ··· 99 99 {/if} 100 100 </p> 101 101 <h3 102 - class="text-base-900 dark:text-base-50 group-hover:text-base-700 dark:group-hover:text-base-200 mt-0.5 line-clamp-2 text-sm leading-snug font-semibold transition-colors sm:text-base" 102 + class="text-base-900 dark:text-base-50 group-hover:text-base-700 dark:group-hover:text-base-200 mt-0.5 line-clamp-2 flex items-start gap-1.5 text-sm leading-snug font-semibold transition-colors sm:text-base" 103 103 > 104 - {event.name} 104 + {#if event.space} 105 + <svg 106 + viewBox="0 0 24 24" 107 + fill="none" 108 + stroke="currentColor" 109 + stroke-width="2" 110 + stroke-linecap="round" 111 + stroke-linejoin="round" 112 + class="text-base-500 dark:text-base-400 mt-0.5 size-3.5 shrink-0" 113 + aria-label="Private event" 114 + > 115 + <rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> 116 + <path d="M7 11V7a5 5 0 0 1 10 0v4" /> 117 + </svg> 118 + {/if} 119 + <span>{event.name}</span> 105 120 </h3> 106 121 {#if location || mode} 107 122 <p class="text-base-500 dark:text-base-400 mt-1 text-xs">
+18 -1
src/lib/components/EventEditor.svelte
··· 42 42 let { 43 43 eventData = null, 44 44 actorDid, 45 - rkey 45 + rkey, 46 + privateMode = false 46 47 }: { 47 48 eventData: FlatEventRecord | null; 48 49 actorDid: string; 49 50 rkey: string; 51 + /** If true, save writes into a permissioned space instead of the user's public PDS. */ 52 + privateMode?: boolean; 50 53 } = $props(); 51 54 52 55 let isNew = $derived(eventData === null); ··· 637 640 // If changed/new but no location, locations stays undefined (removed/absent) 638 641 } else if (eventData?.locations && eventData.locations.length > 0) { 639 642 record.locations = eventData.locations; 643 + } 644 + 645 + if (privateMode) { 646 + const { createPrivateEvent } = await import('$lib/spaces/server/spaces.remote'); 647 + const { spaceUri, rkey: eventRkey } = await createPrivateEvent({ key: rkey, record }); 648 + localStorage.removeItem(DRAFT_KEY); 649 + if (thumbnailKey) deleteImage(thumbnailKey); 650 + const spaceKey = spaceUri.split('/').pop(); 651 + const handle = 652 + user.profile?.handle && user.profile.handle !== 'handle.invalid' 653 + ? user.profile.handle 654 + : user.did; 655 + goto(`/p/${handle}/e/${eventRkey}/s/${spaceKey}?created=true`); 656 + return; 640 657 } 641 658 642 659 const response = await putRecord({
+50 -22
src/lib/components/EventRsvp.svelte
··· 11 11 eventCid, 12 12 initialRsvpStatus = null, 13 13 initialRsvpRkey = null, 14 + spaceUri = null, 14 15 onrsvp, 15 16 oncancel, 16 17 onlogin ··· 19 20 eventCid: string | null; 20 21 initialRsvpStatus?: 'going' | 'interested' | 'notgoing' | null; 21 22 initialRsvpRkey?: string | null; 23 + /** If set, RSVPs write into this space instead of the user's public PDS. */ 24 + spaceUri?: string | null; 22 25 onrsvp?: (status: 'going' | 'interested', rkey: string) => void; 23 26 oncancel?: () => void; 24 27 onlogin?: () => void; ··· 38 41 rsvpSubmitting = true; 39 42 try { 40 43 const key = rsvpRkey ?? createTID(); 44 + const record = { 45 + $type: 'community.lexicon.calendar.rsvp', 46 + createdWith: 'https://atmo.rsvp', 47 + status: `community.lexicon.calendar.rsvp#${status}`, 48 + subject: { 49 + uri: eventUri, 50 + ...(eventCid ? { cid: eventCid } : {}) 51 + }, 52 + createdAt: new Date().toISOString() 53 + }; 41 54 42 - const response = await putRecord({ 43 - collection: 'community.lexicon.calendar.rsvp', 44 - rkey: key, 45 - record: { 46 - $type: 'community.lexicon.calendar.rsvp', 47 - createdWith: 'https://atmo.rsvp', 48 - status: `community.lexicon.calendar.rsvp#${status}`, 49 - subject: { 50 - uri: eventUri, 51 - ...(eventCid ? { cid: eventCid } : {}) 52 - }, 53 - createdAt: new Date().toISOString() 55 + let ok = false; 56 + if (spaceUri) { 57 + const { putSpaceRecord } = await import('$lib/spaces/server/spaces.remote'); 58 + const result = await putSpaceRecord({ 59 + spaceUri, 60 + collection: 'community.lexicon.calendar.rsvp', 61 + rkey: key, 62 + record 63 + }); 64 + ok = !!result; 65 + } else { 66 + const response = await putRecord({ 67 + collection: 'community.lexicon.calendar.rsvp', 68 + rkey: key, 69 + record 70 + }); 71 + ok = response.ok; 72 + if (ok) { 73 + notifyContrailOfUpdate(`at://${user.did}/community.lexicon.calendar.rsvp/${key}`); 54 74 } 55 - }); 75 + } 56 76 57 - if (response.ok) { 58 - const rsvpUri = `at://${user.did}/community.lexicon.calendar.rsvp/${key}`; 59 - notifyContrailOfUpdate(rsvpUri); 77 + if (ok) { 60 78 rsvpStatusOverride = status; 61 79 rsvpRkeyOverride = key; 62 80 launchConfetti(); ··· 73 91 if (!user.isLoggedIn || !user.did || !rsvpRkey) return; 74 92 rsvpSubmitting = true; 75 93 try { 76 - const rsvpUri = `at://${user.did}/community.lexicon.calendar.rsvp/${rsvpRkey}`; 77 - await deleteRecord({ 78 - collection: 'community.lexicon.calendar.rsvp', 79 - rkey: rsvpRkey 80 - }); 81 - notifyContrailOfUpdate(rsvpUri); 94 + if (spaceUri) { 95 + const { deleteSpaceRecord } = await import('$lib/spaces/server/spaces.remote'); 96 + await deleteSpaceRecord({ 97 + spaceUri, 98 + collection: 'community.lexicon.calendar.rsvp', 99 + rkey: rsvpRkey 100 + }); 101 + } else { 102 + await deleteRecord({ 103 + collection: 'community.lexicon.calendar.rsvp', 104 + rkey: rsvpRkey 105 + }); 106 + notifyContrailOfUpdate( 107 + `at://${user.did}/community.lexicon.calendar.rsvp/${rsvpRkey}` 108 + ); 109 + } 82 110 rsvpStatusOverride = null; 83 111 rsvpRkeyOverride = null; 84 112 oncancel?.();
+705
src/lib/components/EventView.svelte
··· 1 + <script lang="ts"> 2 + import { eventUrl, isEventOngoing, type FlatEventRecord } from '$lib/contrail'; 3 + import { getCDNImageBlobUrl } from '$lib/atproto'; 4 + import { user } from '$lib/atproto/auth.svelte'; 5 + import { Avatar as FoxAvatar, Badge, Button } from '@foxui/core'; 6 + import Map from '$lib/components/Map.svelte'; 7 + import ShareModal from '$lib/components/ShareModal.svelte'; 8 + import Avatar from 'svelte-boring-avatars'; 9 + import EventRsvp from '$lib/components/EventRsvp.svelte'; 10 + import EventCard from '$lib/components/EventCard.svelte'; 11 + import EventAttendees from './EventAttendees.svelte'; 12 + import VodPlayer, { type VodPlayerApi } from '$lib/components/VodPlayer.svelte'; 13 + import VodTranscript from '$lib/components/VodTranscript.svelte'; 14 + import { page } from '$app/state'; 15 + import { marked } from 'marked'; 16 + import { sanitize } from '$lib/cal/sanitize'; 17 + import { generateICalEvent } from '$lib/cal/ical'; 18 + import { launchConfetti } from '@foxui/visual'; 19 + import ThemeBackground from '$lib/components/ThemeBackground.svelte'; 20 + import ThemeApply from '$lib/components/ThemeApply.svelte'; 21 + import { defaultTheme, type EventTheme } from '$lib/theme'; 22 + 23 + let { data } = $props(); 24 + 25 + let eventData: FlatEventRecord = $derived(data.eventData); 26 + let did: string = $derived(data.actorDid); 27 + let rkey: string = $derived(data.rkey); 28 + let hostProfile = $derived(data.hostProfile); 29 + let attendees = $derived(data.attendees); 30 + 31 + let theme: EventTheme = $derived(eventData.theme ?? defaultTheme); 32 + 33 + 34 + let hostUrl = $derived(`/p/${hostProfile?.handle || did}`); 35 + let eventPath = $derived(eventUrl(eventData, hostProfile?.handle || did)); 36 + let shareUrl = $derived( 37 + typeof window !== 'undefined' ? `${window.location.origin}${eventPath}` : eventPath 38 + ); 39 + 40 + // Times are always rendered in the viewer's local timezone — the stored UTC 41 + // instant is what the Date constructor parses, and toLocaleString/Time uses 42 + // the browser's zone by default. 43 + let startDate = $derived(new Date(eventData.startsAt)); 44 + let endDate = $derived(eventData.endsAt ? new Date(eventData.endsAt) : null); 45 + 46 + function formatMonth(date: Date): string { 47 + return date.toLocaleDateString('en-US', { month: 'short' }).toUpperCase(); 48 + } 49 + 50 + function formatDay(date: Date): number { 51 + return date.getDate(); 52 + } 53 + 54 + function formatWeekday(date: Date): string { 55 + return date.toLocaleDateString('en-US', { weekday: 'long' }); 56 + } 57 + 58 + function formatFullDate(date: Date): string { 59 + const options: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric' }; 60 + if (date.getFullYear() !== new Date().getFullYear()) { 61 + options.year = 'numeric'; 62 + } 63 + return date.toLocaleDateString('en-US', options); 64 + } 65 + 66 + function formatTime(date: Date): string { 67 + return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); 68 + } 69 + 70 + function getModeLabel(mode: string): string { 71 + if (mode.includes('virtual')) return 'Virtual'; 72 + if (mode.includes('hybrid')) return 'Hybrid'; 73 + if (mode.includes('inperson')) return 'In-Person'; 74 + return 'Event'; 75 + } 76 + 77 + function getModeColor(mode: string): 'cyan' | 'purple' | 'amber' | 'secondary' { 78 + if (mode.includes('virtual')) return 'cyan'; 79 + if (mode.includes('hybrid')) return 'purple'; 80 + if (mode.includes('inperson')) return 'amber'; 81 + return 'secondary'; 82 + } 83 + 84 + function getLocationData(locations: FlatEventRecord['locations']) { 85 + if (!locations || locations.length === 0) return null; 86 + 87 + const loc = locations.find((v) => v.$type === 'community.lexicon.location.address') as 88 + | { name?: string; street?: string; locality?: string; region?: string; country?: string } 89 + | undefined; 90 + if (!loc) return null; 91 + 92 + const shortParts = [loc.street, loc.locality].filter(Boolean); 93 + const fullParts = [loc.street, loc.locality, loc.region, loc.country].filter(Boolean); 94 + if (fullParts.length === 0) return null; 95 + 96 + const shortAddress = shortParts.join(', '); 97 + const fullAddress = fullParts.join(', '); 98 + const displayName = loc.name || undefined; 99 + const fullString = displayName ? `${displayName}, ${fullAddress}` : fullAddress; 100 + const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(fullString)}`; 101 + 102 + return { name: displayName, shortAddress, fullAddress, fullString, mapsUrl }; 103 + } 104 + 105 + let locationData = $derived(getLocationData(eventData.locations)); 106 + let location = $derived(locationData?.fullString); 107 + 108 + let geoLocation: { lat: number; lng: number } | null = $state(null); 109 + 110 + function initGeoLocation() { 111 + if (!eventData.locations || eventData.locations.length === 0) return; 112 + 113 + // Check for explicit geo coordinates first 114 + const geo = eventData.locations.find((v) => v.$type === 'community.lexicon.location.geo') as 115 + | { latitude?: string; longitude?: string } 116 + | undefined; 117 + if (geo?.latitude && geo?.longitude) { 118 + const lat = parseFloat(geo.latitude); 119 + const lng = parseFloat(geo.longitude); 120 + if (!isNaN(lat) && !isNaN(lng)) { 121 + geoLocation = { lat, lng }; 122 + return; 123 + } 124 + } 125 + 126 + // Geocode from address if available 127 + const addressQuery = locationData?.fullAddress; 128 + if (addressQuery) { 129 + fetch(`/api/geocoding?q=${encodeURIComponent(addressQuery)}`) 130 + .then((r) => (r.ok ? r.json() : null)) 131 + .then((data: unknown) => { 132 + const d = data as Record<string, unknown> | null; 133 + if (!d) return; 134 + if (d.lat && d.lon) { 135 + geoLocation = { lat: parseFloat(d.lat as string), lng: parseFloat(d.lon as string) }; 136 + } 137 + }) 138 + .catch(() => {}); 139 + } 140 + } 141 + 142 + let showShareModal = $state(false); 143 + let shareModalTitle = $state('Event created!'); 144 + let shareModalText: string | undefined = $state(undefined); 145 + 146 + import { onMount } from 'svelte'; 147 + onMount(() => { 148 + initGeoLocation(); 149 + 150 + const url = new URL(window.location.href); 151 + if (url.searchParams.has('created')) { 152 + url.searchParams.delete('created'); 153 + history.replaceState({}, '', url.pathname); 154 + launchConfetti(); 155 + shareModalTitle = 'Event created!'; 156 + shareModalText = `I'm hosting "${eventData.name}"!\n\n${shareUrl}`; 157 + showShareModal = true; 158 + } 159 + }); 160 + 161 + let thumbnailImage = $derived.by(() => { 162 + if (!eventData.media || eventData.media.length === 0) return null; 163 + const media = eventData.media.find((m) => m.role === 'thumbnail'); 164 + if (!media?.content) return null; 165 + const url = getCDNImageBlobUrl({ did, blob: media.content }); 166 + if (!url) return null; 167 + return { url, alt: media.alt || eventData.name }; 168 + }); 169 + 170 + let bannerImage = $derived.by(() => { 171 + if (!eventData.media || eventData.media.length === 0) return null; 172 + const media = eventData.media.find((m) => m.role === 'header'); 173 + if (!media?.content) return null; 174 + const url = getCDNImageBlobUrl({ did, blob: media.content }); 175 + if (!url) return null; 176 + return { url, alt: media.alt || eventData.name }; 177 + }); 178 + 179 + // Prefer thumbnail; fall back to header/banner image 180 + let displayImage = $derived(thumbnailImage ?? bannerImage); 181 + let isBannerOnly = $derived(!thumbnailImage && !!bannerImage); 182 + 183 + let isSameDay = $derived( 184 + endDate && 185 + startDate.getFullYear() === endDate.getFullYear() && 186 + startDate.getMonth() === endDate.getMonth() && 187 + startDate.getDate() === endDate.getDate() 188 + ); 189 + 190 + let isOngoing = $derived(isEventOngoing(eventData.startsAt, eventData.endsAt)); 191 + let isPast = $derived(endDate ? endDate < new Date() : false); 192 + 193 + const renderer = new marked.Renderer(); 194 + renderer.link = ({ href, text }) => 195 + `<a target="_blank" rel="noopener noreferrer nofollow" href="${href}" class="text-accent-600 dark:text-accent-400 hover:underline">${text}</a>`; 196 + 197 + function renderDescription( 198 + text: string, 199 + facets?: { 200 + index: { byteStart: number; byteEnd: number }; 201 + features: { $type: string; did?: string; uri?: string; tag?: string }[]; 202 + }[] 203 + ): string { 204 + let result = text; 205 + 206 + if (facets && facets.length > 0) { 207 + const encoder = new TextEncoder(); 208 + const encoded = encoder.encode(text); 209 + const decoder = new TextDecoder(); 210 + 211 + // Sort facets in reverse order by byteStart so replacements don't shift positions 212 + const sorted = [...facets].sort((a, b) => b.index.byteStart - a.index.byteStart); 213 + 214 + for (const facet of sorted) { 215 + const feature = facet.features?.[0]; 216 + if (!feature) continue; 217 + 218 + const segmentBytes = encoded.slice(facet.index.byteStart, facet.index.byteEnd); 219 + const segmentText = decoder.decode(segmentBytes); 220 + 221 + let mdLink: string | null = null; 222 + switch (feature.$type) { 223 + case 'app.bsky.richtext.facet#mention': 224 + mdLink = `[${segmentText}](/${feature.did})`; 225 + break; 226 + case 'app.bsky.richtext.facet#link': 227 + mdLink = `[${segmentText}](${feature.uri})`; 228 + break; 229 + case 'app.bsky.richtext.facet#tag': 230 + mdLink = `[${segmentText}](https://bsky.app/hashtag/${feature.tag})`; 231 + break; 232 + } 233 + 234 + if (mdLink) { 235 + // Convert byte offsets to character offsets for string replacement 236 + const before = decoder.decode(encoded.slice(0, facet.index.byteStart)); 237 + const after = decoder.decode(encoded.slice(facet.index.byteEnd)); 238 + result = before + mdLink + after; 239 + } 240 + } 241 + } 242 + 243 + return marked.parse(result, { renderer }) as string; 244 + } 245 + 246 + let descriptionHtml = $derived( 247 + eventData.description 248 + ? sanitize( 249 + renderDescription( 250 + eventData.description, 251 + eventData.facets as 252 + | { 253 + index: { byteStart: number; byteEnd: number }; 254 + features: { $type: string; did?: string; uri?: string; tag?: string }[]; 255 + }[] 256 + | undefined 257 + ), 258 + { ADD_ATTR: ['target'] } 259 + ) 260 + : null 261 + ); 262 + 263 + let eventUri = $derived(`at://${did}/community.lexicon.calendar.event/${rkey}`); 264 + 265 + let ogImageUrl = $derived(`${page.url.origin}${page.url.pathname}/og.png`); 266 + 267 + let isOwner = $derived(user.isLoggedIn && user.did === did); 268 + 269 + let speakers = $derived(data.speakerProfiles ?? []); 270 + 271 + let vodCurrentTime = $state(0); 272 + let vodApi: VodPlayerApi | undefined = $state(); 273 + 274 + let attendeesRef: EventAttendees | undefined = $state(); 275 + 276 + function handleRsvp(status: 'going' | 'interested') { 277 + if (!user.did) return; 278 + attendeesRef?.addAttendee({ 279 + did: user.did, 280 + status, 281 + avatar: user.profile?.avatar, 282 + name: user.profile?.displayName || user.profile?.handle || user.did, 283 + handle: user.profile?.handle, 284 + url: `/${user.profile?.handle || user.did}` 285 + }); 286 + if (status === 'interested') return; 287 + shareModalTitle = "You're going!"; 288 + shareModalText = `I'm going to "${eventData.name}".\n\n${shareUrl}`; 289 + showShareModal = true; 290 + } 291 + 292 + function handleRsvpCancel() { 293 + if (!user.did) return; 294 + attendeesRef?.removeAttendee(user.did); 295 + } 296 + 297 + function downloadIcs() { 298 + const ical = generateICalEvent(eventData, eventUri, page.url.href); 299 + const blob = new Blob([ical], { type: 'text/calendar;charset=utf-8' }); 300 + const url = URL.createObjectURL(blob); 301 + const a = document.createElement('a'); 302 + a.href = url; 303 + a.download = `${eventData.name.replace(/[^a-zA-Z0-9]/g, '-')}.ics`; 304 + a.click(); 305 + URL.revokeObjectURL(url); 306 + } 307 + </script> 308 + 309 + <svelte:head> 310 + <title>{eventData.name}</title> 311 + <meta name="description" content={eventData.description || `Event: ${eventData.name}`} /> 312 + <meta property="og:title" content={eventData.name} /> 313 + <meta property="og:description" content={eventData.description || `Event: ${eventData.name}`} /> 314 + <meta name="twitter:card" content="summary_large_image" /> 315 + <meta name="twitter:title" content={eventData.name} /> 316 + <meta name="twitter:description" content={eventData.description || `Event: ${eventData.name}`} /> 317 + <meta name="twitter:image" content={ogImageUrl} /> 318 + </svelte:head> 319 + 320 + <ThemeApply accentColor={theme.accentColor} baseColor={theme.baseColor} /> 321 + <ThemeBackground {theme} /> 322 + 323 + <div class="min-h-screen px-6 py-12 sm:py-12"> 324 + <div class="mx-auto max-w-3xl"> 325 + <!-- Banner image (full width, only when no thumbnail) --> 326 + {#if isBannerOnly && displayImage} 327 + <img 328 + src={displayImage.url} 329 + alt={displayImage.alt} 330 + class="border-base-200 dark:border-base-800 mb-8 aspect-3/1 w-full rounded-2xl border object-cover" 331 + /> 332 + {/if} 333 + 334 + <!-- Two-column layout: image left, details right --> 335 + <div 336 + class="grid grid-cols-1 gap-8 md:grid-cols-[14rem_1fr] md:grid-rows-[auto_1fr] md:gap-x-10 md:gap-y-6 lg:grid-cols-[16rem_1fr]" 337 + > 338 + <!-- Thumbnail image (left column) --> 339 + {#if !isBannerOnly} 340 + <div class="order-1 max-w-sm md:order-0 md:col-start-1 md:max-w-none"> 341 + {#if displayImage} 342 + <img 343 + src={displayImage.url} 344 + alt={displayImage.alt} 345 + class="border-base-200 dark:border-base-800 bg-base-200 dark:bg-base-950/50 aspect-square w-full rounded-2xl border object-cover" 346 + /> 347 + {:else} 348 + <div 349 + class="border-base-200 dark:border-base-800 aspect-square w-full overflow-hidden rounded-2xl border [&>svg]:h-full [&>svg]:w-full" 350 + > 351 + <Avatar 352 + size={256} 353 + name={data.rkey} 354 + variant="marble" 355 + colors={['#92A1C6', '#146A7C', '#F0AB3D', '#C271B4', '#C20D90']} 356 + square 357 + /> 358 + </div> 359 + {/if} 360 + {#if isOwner} 361 + <Button href="./{rkey}/edit" class="mt-9 w-full">Edit Event</Button> 362 + {/if} 363 + </div> 364 + {/if} 365 + 366 + <!-- Right column: event details --> 367 + <div class="order-2 min-w-0 md:order-0 md:col-start-2 md:row-span-2 md:row-start-1"> 368 + <div class="mb-2"> 369 + <h1 class="text-base-900 dark:text-base-50 text-3xl leading-tight font-bold sm:text-4xl"> 370 + {eventData.name} 371 + </h1> 372 + </div> 373 + 374 + <!-- Badges --> 375 + {#if eventData.mode || isOngoing} 376 + <div class="mb-8 flex items-center gap-2"> 377 + {#if isOngoing} 378 + <Badge size="md" variant="primary"> 379 + <span class="bg-accent-500 mr-1 inline-block size-1.5 animate-pulse rounded-full" 380 + ></span> 381 + Live 382 + </Badge> 383 + {/if} 384 + {#if eventData.mode} 385 + <Badge size="md" variant={getModeColor(eventData.mode)} 386 + >{getModeLabel(eventData.mode)}</Badge 387 + > 388 + {/if} 389 + </div> 390 + {/if} 391 + 392 + <!-- Date row --> 393 + <div class="mb-4 flex items-center gap-4"> 394 + <div 395 + class="border-base-200 dark:border-base-700 bg-base-100 dark:bg-base-950/30 flex size-12 shrink-0 flex-col items-center justify-center overflow-hidden rounded-xl border" 396 + > 397 + <span class="text-base-500 dark:text-base-400 text-[9px] leading-none font-semibold"> 398 + {formatMonth(startDate)} 399 + </span> 400 + <span class="text-base-900 dark:text-base-50 text-lg leading-tight font-bold"> 401 + {formatDay(startDate)} 402 + </span> 403 + </div> 404 + <div> 405 + <p class="text-base-900 dark:text-base-50 font-semibold"> 406 + {formatWeekday(startDate)}, {formatFullDate(startDate)} 407 + {#if endDate && !isSameDay} 408 + - {formatWeekday(endDate)}, {formatFullDate(endDate)} 409 + {/if} 410 + </p> 411 + <p class="text-base-500 dark:text-base-400 text-sm"> 412 + {formatTime(startDate)} 413 + {#if endDate && isSameDay} 414 + - {formatTime(endDate)} 415 + {/if} 416 + </p> 417 + </div> 418 + </div> 419 + 420 + <!-- Location row --> 421 + {#if locationData} 422 + <a 423 + href={locationData.mapsUrl} 424 + target="_blank" 425 + rel="noopener noreferrer" 426 + class="mb-6 flex items-center gap-4 transition-opacity hover:opacity-80" 427 + > 428 + <div 429 + class="border-base-200 dark:border-base-700 bg-base-100 dark:bg-base-950/30 flex size-12 shrink-0 items-center justify-center rounded-xl border" 430 + > 431 + <svg 432 + xmlns="http://www.w3.org/2000/svg" 433 + fill="none" 434 + viewBox="0 0 24 24" 435 + stroke-width="1.5" 436 + stroke="currentColor" 437 + class="text-base-900 dark:text-base-200 size-5" 438 + > 439 + <path 440 + stroke-linecap="round" 441 + stroke-linejoin="round" 442 + d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 443 + /> 444 + <path 445 + stroke-linecap="round" 446 + stroke-linejoin="round" 447 + d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z" 448 + /> 449 + </svg> 450 + </div> 451 + <div> 452 + {#if locationData.name} 453 + <p class="text-base-900 dark:text-base-50 font-semibold">{locationData.name}</p> 454 + <p class="text-base-500 dark:text-base-400 text-sm">{locationData.shortAddress}</p> 455 + {:else} 456 + <p class="text-base-900 dark:text-base-50 font-semibold"> 457 + {locationData.shortAddress} 458 + </p> 459 + {/if} 460 + </div> 461 + </a> 462 + {/if} 463 + 464 + <!-- Part of --> 465 + {#if data.parentEvent} 466 + <div 467 + class="border-base-200 dark:border-base-800 bg-base-100 dark:bg-base-950/50 mt-8 mb-2 justify-center rounded-2xl border p-4" 468 + > 469 + <p 470 + class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 471 + > 472 + Part of 473 + </p> 474 + <EventCard event={data.parentEvent} actor="atprotocol.dev" /> 475 + <Button href="/p/atmosphereconf.org" size="lg" class="mt-6 w-full"> 476 + See full schedule 477 + </Button> 478 + </div> 479 + {/if} 480 + 481 + {#if did === 'did:plc:lehcqqkwzcwvjvw66uthu5oq' && rkey === '3lte3c7x43l2e'} 482 + <Button href="/p/atmosphereconf.org" size="lg" class="mb-4 w-full"> 483 + See full schedule 484 + </Button> 485 + {/if} 486 + 487 + {#if !isPast} 488 + <EventRsvp 489 + {eventUri} 490 + eventCid={eventData.cid ?? null} 491 + initialRsvpStatus={data.viewerRsvpStatus} 492 + initialRsvpRkey={data.viewerRsvpRkey} 493 + spaceUri={data.spaceUri ?? null} 494 + onrsvp={handleRsvp} 495 + oncancel={handleRsvpCancel} 496 + /> 497 + {/if} 498 + 499 + <!-- About Event --> 500 + {#if descriptionHtml} 501 + <div class="mt-8 mb-8"> 502 + <p 503 + class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 504 + > 505 + About 506 + </p> 507 + <div 508 + class="text-base-700 dark:text-base-300 prose dark:prose-invert prose-a:text-accent-600 dark:prose-a:text-accent-400 prose-a:hover:underline prose-a:no-underline max-w-none leading-relaxed wrap-break-word" 509 + > 510 + {@html descriptionHtml} 511 + </div> 512 + </div> 513 + {/if} 514 + 515 + <!-- Recording --> 516 + {#if data.vod} 517 + <div class="mt-8 mb-8"> 518 + <p 519 + class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 520 + > 521 + Recording 522 + </p> 523 + <VodPlayer 524 + playlistUrl={data.vod.playlistUrl} 525 + title={eventData.name} 526 + subtitlesUrl="/vods/{rkey}-karaoke.vtt" 527 + bind:currentTime={vodCurrentTime} 528 + bind:api={vodApi} 529 + /> 530 + </div> 531 + 532 + <!-- <div class="mt-4 mb-8"> 533 + <p 534 + class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 535 + > 536 + Transcript 537 + </p> 538 + <VodTranscript 539 + transcriptUrl="/vods/{rkey}.json" 540 + currentTime={vodCurrentTime} 541 + onseek={(time) => vodApi?.seek(time)} 542 + /> 543 + </div> --> 544 + {/if} 545 + 546 + <!-- Map --> 547 + {#if geoLocation && locationData} 548 + <div class="mt-8 mb-8"> 549 + <p 550 + class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 551 + > 552 + Location 553 + </p> 554 + <a 555 + href={locationData.mapsUrl} 556 + target="_blank" 557 + rel="noopener noreferrer" 558 + class="block transition-opacity hover:opacity-80" 559 + > 560 + <div class="h-64 w-full overflow-hidden rounded-xl"> 561 + <Map lat={geoLocation.lat} lng={geoLocation.lng} /> 562 + </div> 563 + <p class="text-base-500 dark:text-base-400 mt-2 text-sm"> 564 + {locationData.fullString} 565 + </p> 566 + </a> 567 + </div> 568 + {/if} 569 + </div> 570 + 571 + <!-- Left column: sidebar info --> 572 + <div class="order-3 space-y-6 md:order-0 md:col-start-1"> 573 + <!-- Hosted By --> 574 + <div> 575 + <p 576 + class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 577 + > 578 + Hosted By 579 + </p> 580 + <a 581 + href={hostUrl} 582 + class="text-base-900 dark:text-base-100 flex items-center gap-2.5 font-medium transition-opacity hover:opacity-80" 583 + > 584 + <FoxAvatar 585 + src={hostProfile?.avatar} 586 + alt={hostProfile?.displayName || hostProfile?.handle || did} 587 + class="size-8 shrink-0" 588 + /> 589 + <span class="truncate text-sm"> 590 + {hostProfile?.displayName || hostProfile?.handle || did} 591 + </span> 592 + </a> 593 + </div> 594 + 595 + <!-- Speakers --> 596 + {#if speakers.length > 0} 597 + <div> 598 + <p 599 + class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 600 + > 601 + Speakers 602 + </p> 603 + <div class="space-y-2"> 604 + {#each speakers as speaker, i (speaker.id || i)} 605 + {#if speaker.handle} 606 + <a 607 + href="/p/{speaker.handle}" 608 + class="text-base-900 dark:text-base-100 flex items-center gap-2.5 font-medium transition-opacity hover:opacity-80" 609 + > 610 + <FoxAvatar src={speaker.avatar} alt={speaker.name} class="size-8 shrink-0" /> 611 + <span class="truncate text-sm">{speaker.name}</span> 612 + </a> 613 + {:else} 614 + <div 615 + class="text-base-900 dark:text-base-100 flex items-center gap-2.5 font-medium" 616 + > 617 + <FoxAvatar alt={speaker.name} class="size-8 shrink-0" /> 618 + <span class="truncate text-sm">{speaker.name}</span> 619 + </div> 620 + {/if} 621 + {/each} 622 + </div> 623 + </div> 624 + {/if} 625 + 626 + {#if eventData.uris && eventData.uris.length > 0} 627 + <!-- Links --> 628 + <div> 629 + <p 630 + class="text-base-500 dark:text-base-400 mb-4 text-xs font-semibold tracking-wider uppercase" 631 + > 632 + Links 633 + </p> 634 + <div class="space-y-3"> 635 + {#each eventData.uris as link (link.name + link.uri)} 636 + <a 637 + href={link.uri} 638 + target="_blank" 639 + rel="noopener noreferrer" 640 + class="text-base-700 dark:text-base-300 hover:text-base-900 dark:hover:text-base-100 flex items-center gap-1.5 text-sm transition-colors" 641 + > 642 + <svg 643 + xmlns="http://www.w3.org/2000/svg" 644 + fill="none" 645 + viewBox="0 0 24 24" 646 + stroke-width="1.5" 647 + stroke="currentColor" 648 + class="size-3.5 shrink-0" 649 + > 650 + <path 651 + stroke-linecap="round" 652 + stroke-linejoin="round" 653 + d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 654 + /> 655 + </svg> 656 + <span class="truncate">{link.name || link.uri.replace(/^https?:\/\//, '')}</span> 657 + </a> 658 + {/each} 659 + </div> 660 + </div> 661 + {/if} 662 + 663 + <!-- Add to Calendar --> 664 + <button 665 + onclick={downloadIcs} 666 + class="text-base-700 dark:text-base-300 hover:text-base-900 dark:hover:text-base-100 flex cursor-pointer items-center gap-2 text-sm font-medium transition-colors" 667 + > 668 + <svg 669 + xmlns="http://www.w3.org/2000/svg" 670 + fill="none" 671 + viewBox="0 0 24 24" 672 + stroke-width="1.5" 673 + stroke="currentColor" 674 + class="size-4" 675 + > 676 + <path 677 + stroke-linecap="round" 678 + stroke-linejoin="round" 679 + d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" 680 + /> 681 + </svg> 682 + Add to Calendar 683 + </button> 684 + 685 + <!-- Attendees --> 686 + <EventAttendees 687 + bind:this={attendeesRef} 688 + going={attendees.going} 689 + interested={attendees.interested} 690 + goingCount={attendees.goingCount} 691 + interestedCount={attendees.interestedCount} 692 + /> 693 + </div> 694 + </div> 695 + </div> 696 + </div> 697 + 698 + <ShareModal 699 + bind:open={showShareModal} 700 + url={shareUrl} 701 + title={shareModalTitle} 702 + shareText={shareModalText} 703 + eventName={eventData.name} 704 + {ogImageUrl} 705 + />
+36 -19
src/lib/contrail.ts
··· 2 2 import type { EventData } from '$lib/event-types'; 3 3 import type { 4 4 RsvpAtmoGetProfile, 5 - CommunityLexiconCalendarEventGetRecord, 6 - CommunityLexiconCalendarEventListRecords, 7 - CommunityLexiconCalendarRsvpListRecords 5 + RsvpAtmoEventGetRecord, 6 + RsvpAtmoEventListRecords, 7 + RsvpAtmoRsvpListRecords 8 8 } from '../lexicon-types'; 9 9 import type { Client } from '@atcute/client'; 10 10 import type { ActorIdentifier } from '@atcute/lexicons'; ··· 16 16 export const RSVP_INTERESTED = 'community.lexicon.calendar.rsvp#interested'; 17 17 18 18 type ProfileOutput = RsvpAtmoGetProfile.$output; 19 - type EventListOutput = CommunityLexiconCalendarEventListRecords.$output; 20 - type EventListRecord = CommunityLexiconCalendarEventListRecords.Record; 21 - type EventProfileEntry = CommunityLexiconCalendarEventListRecords.ProfileEntry; 22 - type EventGetOutput = CommunityLexiconCalendarEventGetRecord.$output; 23 - type EventGetProfileEntry = CommunityLexiconCalendarEventGetRecord.ProfileEntry; 24 - type RsvpListRecord = CommunityLexiconCalendarRsvpListRecords.Record; 25 - type RsvpProfileEntry = CommunityLexiconCalendarRsvpListRecords.ProfileEntry; 26 - type HydratedEventRecord = CommunityLexiconCalendarRsvpListRecords.RefEventRecord; 19 + type EventListOutput = RsvpAtmoEventListRecords.$output; 20 + type EventListRecord = RsvpAtmoEventListRecords.Record; 21 + type EventProfileEntry = RsvpAtmoEventListRecords.ProfileEntry; 22 + type EventGetOutput = RsvpAtmoEventGetRecord.$output; 23 + type EventGetProfileEntry = RsvpAtmoEventGetRecord.ProfileEntry; 24 + type RsvpListRecord = RsvpAtmoRsvpListRecords.Record; 25 + type RsvpProfileEntry = RsvpAtmoRsvpListRecords.ProfileEntry; 26 + type HydratedEventRecord = RsvpAtmoRsvpListRecords.RefEventRecord; 27 27 type FlattenableEventRecord = EventListRecord | EventGetOutput | HydratedEventRecord; 28 28 type EventProfiles = EventProfileEntry[] | EventGetProfileEntry[] | undefined; 29 29 type EventRsvps = EventListRecord['rsvps'] | EventGetOutput['rsvps']; ··· 33 33 did: string; 34 34 rkey: string; 35 35 uri: string; 36 + /** Populated when the event was read from a permissioned space. */ 37 + space?: string; 36 38 rsvps?: EventRsvps; 37 39 rsvpsCount?: number; 38 40 rsvpsGoingCount?: number; ··· 113 115 did: record.did, 114 116 rkey: record.rkey, 115 117 uri: record.uri, 118 + ...('space' in record && typeof record.space === 'string' ? { space: record.space } : {}), 116 119 ...('rsvps' in record ? { rsvps: record.rsvps } : {}), 117 120 ...('rsvpsCount' in record ? { rsvpsCount: record.rsvpsCount } : {}), 118 121 ...('rsvpsGoingCount' in record ? { rsvpsGoingCount: record.rsvpsGoingCount } : {}), ··· 129 132 .filter((record): record is FlatEventRecord => record !== null); 130 133 } 131 134 135 + /** Build the canonical path for an event. Private events (those with a `space` 136 + * field from contrail's union) live under `/p/<actor>/e/<rkey>/s/<skey>` so 137 + * the page knows both which event to show and which space to look in. Public 138 + * events use `/p/<actor>/e/<rkey>`. */ 139 + export function eventUrl(event: FlatEventRecord, actor?: string): string { 140 + const who = actor || event.did; 141 + if (event.space) { 142 + const m = event.space.match(/^at:\/\/[^/]+\/[^/]+\/([^/]+)$/); 143 + const skey = m?.[1]; 144 + if (skey) return `/p/${who}/e/${event.rkey}/s/${skey}`; 145 + } 146 + return `/p/${who}/e/${event.rkey}`; 147 + } 148 + 132 149 export function getHostProfile(did: string, profiles?: EventProfiles): HostProfile | null { 133 150 const profile = profiles?.find((entry) => entry.did === did); 134 151 if (!profile) return null; ··· 215 232 export async function getProfileFromContrail( 216 233 client: Client, 217 234 actor: ActorIdentifier 218 - ): Promise<ProfileOutput | null> { 235 + ): Promise<ProfileOutput['profiles'][number] | null> { 219 236 const response = await client.get('rsvp.atmo.getProfile', { 220 237 params: { actor } 221 238 }); 222 239 223 240 if (!response.ok) return null; 224 - return response.data; 241 + return response.data.profiles?.[0] ?? null; 225 242 } 226 243 227 244 export async function listEventRecordsFromContrail( 228 245 client: Client, 229 246 params: ListEventsParams 230 247 ): Promise<EventListOutput | null> { 231 - const response = await client.get('community.lexicon.calendar.event.listRecords', { 248 + const response = await client.get('rsvp.atmo.event.listRecords', { 232 249 params 233 250 }); 234 251 ··· 250 267 profiles?: boolean; 251 268 } 252 269 ): Promise<EventGetOutput | null> { 253 - const response = await client.get('community.lexicon.calendar.event.getRecord', { 270 + const response = await client.get('rsvp.atmo.event.getRecord', { 254 271 params: { 255 272 uri: `at://${did}/community.lexicon.calendar.event/${rkey}`, 256 273 ...(hydrateRsvps ? { hydrateRsvps } : {}), ··· 272 289 actor: ActorIdentifier; 273 290 } 274 291 ): Promise<RsvpListRecord | null> { 275 - const response = await client.get('community.lexicon.calendar.rsvp.listRecords', { 292 + const response = await client.get('rsvp.atmo.rsvp.listRecords', { 276 293 params: { 277 294 actor, 278 295 subjectUri: eventUri, ··· 289 306 eventUri: string 290 307 ): Promise<EventAttendeesResult> { 291 308 const [goingResponse, interestedResponse] = await Promise.all([ 292 - client.get('community.lexicon.calendar.rsvp.listRecords', { 309 + client.get('rsvp.atmo.rsvp.listRecords', { 293 310 params: { 294 311 subjectUri: eventUri, 295 312 status: RSVP_GOING, ··· 297 314 limit: 200 298 315 } 299 316 }), 300 - client.get('community.lexicon.calendar.rsvp.listRecords', { 317 + client.get('rsvp.atmo.rsvp.listRecords', { 301 318 params: { 302 319 subjectUri: eventUri, 303 320 status: RSVP_INTERESTED, ··· 337 354 } 338 355 339 356 export async function listAttendingEventsFromContrail(client: Client, actor: ActorIdentifier) { 340 - const response = await client.get('community.lexicon.calendar.rsvp.listRecords', { 357 + const response = await client.get('rsvp.atmo.rsvp.listRecords', { 341 358 params: { 342 359 actor, 343 360 hydrateEvent: true,
+19 -4
src/lib/contrail/config.ts
··· 1 1 import type { ContrailConfig } from '@atmo-dev/contrail'; 2 + import { SPACE_TYPE } from '../spaces/config'; 2 3 3 4 export const config: ContrailConfig = { 4 5 namespace: 'rsvp.atmo', 6 + // `spaces` is declared statically so `pnpm generate` emits the `rsvp.atmo.space.*` 7 + // lexicons. The real serviceDid is injected at runtime in `$lib/contrail/index.ts` 8 + // via `getSpacesConfig()` — generate doesn't serialize it. 9 + spaces: { type: SPACE_TYPE, serviceDid: 'did:web:placeholder' }, 10 + permissionSet: { 11 + title: 'Atmo Events', 12 + description: 'Manage your private events and rsvps.' 13 + // NOTE: permission-set lexicons can only reference NSIDs under their own 14 + // namespace (`rsvp.atmo.*`). Repo writes for `community.lexicon.*` and 15 + // blob uploads are declared as standalone `scope.repo(...)` / 16 + // `scope.blob(...)` entries in `atproto/settings.ts`, not here. 17 + }, 5 18 collections: { 6 - 'community.lexicon.calendar.event': { 19 + event: { 20 + collection: 'community.lexicon.calendar.event', 7 21 queryable: { 8 22 mode: {}, 9 23 name: {}, ··· 16 30 searchable: ['mode', 'name', 'status', 'description'], 17 31 relations: { 18 32 rsvps: { 19 - collection: 'community.lexicon.calendar.rsvp', 33 + collection: 'rsvp', 20 34 groupBy: 'status', 21 35 groups: { 22 36 going: 'community.lexicon.calendar.rsvp#going', ··· 26 40 } 27 41 } 28 42 }, 29 - 'community.lexicon.calendar.rsvp': { 43 + rsvp: { 44 + collection: 'community.lexicon.calendar.rsvp', 30 45 queryable: { 31 46 status: {}, 32 47 'subject.uri': {} 33 48 }, 34 49 references: { 35 50 event: { 36 - collection: 'community.lexicon.calendar.event', 51 + collection: 'event', 37 52 field: 'subject.uri' 38 53 } 39 54 }
+1 -20
src/lib/spaces/config.ts
··· 1 1 import type { SpacesConfig } from '@atmo-dev/contrail'; 2 - import { 3 - CompositeDidDocumentResolver, 4 - PlcDidDocumentResolver, 5 - WebDidDocumentResolver 6 - } from '@atcute/identity-resolver'; 7 2 import { SERVICE_DID, SERVICE_URL } from './tunnel-service.generated'; 8 3 9 4 /** The NSID identifying our kind of permissioned space. */ 10 5 export const SPACE_TYPE = 'tools.atmo.event.space'; 11 6 12 - /** Per-collection policy for event-space records. */ 13 - const DEFAULT_POLICIES = { 14 - 'community.lexicon.calendar.event': { read: 'member' as const, write: 'owner' as const }, 15 - 'community.lexicon.calendar.rsvp': { read: 'member' as const, write: 'member' as const }, 16 - 'app.event.message': { read: 'member' as const, write: 'member' as const } 17 - }; 18 - 19 7 /** Build the spaces config for contrail, or null if we can't run spaces 20 8 * (no service DID => dev without tunnel, prod before service is published). */ 21 9 export function getSpacesConfig(): SpacesConfig | null { ··· 24 12 } 25 13 return { 26 14 type: SPACE_TYPE, 27 - serviceDid: SERVICE_DID, 28 - resolver: new CompositeDidDocumentResolver({ 29 - methods: { 30 - plc: new PlcDidDocumentResolver(), 31 - web: new WebDidDocumentResolver() 32 - } 33 - }), 34 - defaultPolicies: DEFAULT_POLICIES 15 + serviceDid: SERVICE_DID 35 16 }; 36 17 } 37 18
+66
src/lib/spaces/server/client.ts
··· 1 + import type { Client } from '@atcute/client'; 2 + import { Client as AtcuteClient } from '@atcute/client'; 3 + import { createHandler } from '@atmo-dev/contrail/server'; 4 + import { contrail, ensureInit } from '$lib/contrail/index'; 5 + import { SERVICE_DID } from '../config'; 6 + 7 + // Register lexicon ambient types (atmo-events/generated) 8 + import '../../../lexicon-types/index.js'; 9 + 10 + const handle = createHandler(contrail); 11 + 12 + /** Cache-key per (did→lxm). Keyed per-request client to avoid bleed across users. */ 13 + function makeJwtCache() { 14 + return new Map<string, { token: string; expiresAt: number }>(); 15 + } 16 + 17 + async function mintServiceJwt( 18 + oauthClient: Client, 19 + aud: string, 20 + lxm: string 21 + ): Promise<string> { 22 + const response = await oauthClient.get('com.atproto.server.getServiceAuth', { 23 + params: { 24 + aud: aud as `did:${string}:${string}`, 25 + lxm: lxm as `${string}.${string}.${string}`, 26 + exp: Math.floor(Date.now() / 1000) + 300 27 + } 28 + }); 29 + if (!response.ok) { 30 + throw new Error( 31 + `getServiceAuth failed: ${response.status} ${JSON.stringify(response.data)}` 32 + ); 33 + } 34 + return response.data.token; 35 + } 36 + 37 + /** Build a typed @atcute/client that routes rsvp.atmo.* calls through 38 + * contrail's handler in-process, attaching a real service-auth JWT per request. 39 + * Each JWT is cached for ~4 minutes to avoid hammering the user's PDS. */ 40 + export function getSpacesClient(oauthClient: Client, db: D1Database): Client { 41 + if (!SERVICE_DID) { 42 + throw new Error('Spaces not configured (no SERVICE_DID). Run `pnpm tunnel` in dev.'); 43 + } 44 + const aud = SERVICE_DID; 45 + const jwtCache = makeJwtCache(); 46 + 47 + async function jwtFor(lxm: string): Promise<string> { 48 + const cached = jwtCache.get(lxm); 49 + if (cached && cached.expiresAt > Date.now() + 10_000) return cached.token; 50 + const token = await mintServiceJwt(oauthClient, aud, lxm); 51 + jwtCache.set(lxm, { token, expiresAt: Date.now() + 250_000 }); 52 + return token; 53 + } 54 + 55 + return new AtcuteClient({ 56 + handler: async (pathname, init) => { 57 + await ensureInit(db); 58 + const url = new URL(pathname, 'http://localhost'); 59 + const nsid = url.pathname.replace(/^\/xrpc\//, ''); 60 + const token = await jwtFor(nsid); 61 + const headers = new Headers(init?.headers as HeadersInit); 62 + headers.set('Authorization', `Bearer ${token}`); 63 + return handle(new Request(url, { ...init, headers }), db) as Promise<Response>; 64 + } 65 + }); 66 + }
+246
src/lib/spaces/server/spaces.remote.ts
··· 1 + import { error } from '@sveltejs/kit'; 2 + import { command, query, getRequestEvent } from '$app/server'; 3 + import * as v from 'valibot'; 4 + import '../../../lexicon-types/index.js'; 5 + import { getSpacesClient } from './client'; 6 + import { SPACE_TYPE, spacesAvailable } from '../config'; 7 + 8 + const atUriSchema = v.pipe(v.string(), v.regex(/^at:\/\/.+/)); 9 + const didSchema = v.pipe(v.string(), v.regex(/^did:[a-z]+:.+/)); 10 + const nsidSchema = v.pipe(v.string(), v.regex(/^[a-zA-Z][a-zA-Z0-9-]*(\.[a-zA-Z][a-zA-Z0-9-]*){2,}$/)); 11 + 12 + function getClient() { 13 + const { locals, platform } = getRequestEvent(); 14 + if (!spacesAvailable()) error(503, 'Spaces are not configured in this environment'); 15 + if (!locals.client || !locals.did) error(401, 'Not authenticated'); 16 + if (!platform?.env.DB) error(500, 'No database binding'); 17 + return { client: getSpacesClient(locals.client, platform.env.DB), did: locals.did }; 18 + } 19 + 20 + // ── Spaces ──────────────────────────────────────────────────────── 21 + 22 + /** Create a private event: makes a space owned by the caller and writes the event record inside it. */ 23 + export const createPrivateEvent = command( 24 + v.object({ 25 + key: v.optional(v.string()), 26 + record: v.record(v.string(), v.unknown()) 27 + }), 28 + async (input) => { 29 + const { client } = getClient(); 30 + 31 + const createRes = await client.post('rsvp.atmo.space.admin.createSpace', { 32 + input: { type: SPACE_TYPE, key: input.key } 33 + }); 34 + if (!createRes.ok) error(500, 'createSpace failed'); 35 + const spaceUri = createRes.data.space.uri; 36 + 37 + const putRes = await client.post('rsvp.atmo.space.putRecord', { 38 + input: { 39 + spaceUri, 40 + collection: 'community.lexicon.calendar.event', 41 + record: input.record 42 + } 43 + }); 44 + if (!putRes.ok) error(500, 'putRecord failed'); 45 + 46 + return { spaceUri, rkey: putRes.data.rkey }; 47 + } 48 + ); 49 + 50 + export const listMyPrivateSpaces = query(async () => { 51 + const { client } = getClient(); 52 + const res = await client.get('rsvp.atmo.space.listSpaces', { 53 + params: { scope: 'owner', type: SPACE_TYPE } 54 + }); 55 + if (!res.ok) error(500, 'listSpaces failed'); 56 + return res.data.spaces; 57 + }); 58 + 59 + export const listMySpaceMemberships = query(async () => { 60 + const { client } = getClient(); 61 + const res = await client.get('rsvp.atmo.space.listSpaces', { 62 + params: { scope: 'member', type: SPACE_TYPE } 63 + }); 64 + if (!res.ok) error(500, 'listSpaces failed'); 65 + return res.data.spaces; 66 + }); 67 + 68 + export const getPrivateSpace = query(v.object({ spaceUri: atUriSchema }), async ({ spaceUri }) => { 69 + const { client } = getClient(); 70 + const spaceRes = await client.get('rsvp.atmo.space.getSpace', { 71 + params: { uri: spaceUri as unknown as import('@atcute/lexicons').ResourceUri } 72 + }); 73 + // Return ok:false instead of throwing — callers (load functions) need to branch on 74 + // access state, and SvelteKit's error() registers with request-level error tracking 75 + // even when caught, which would show the default error page. 76 + if (!spaceRes.ok) return { ok: false as const, status: spaceRes.status }; 77 + 78 + const [eventsRes, rsvpsRes] = await Promise.all([ 79 + client.get('rsvp.atmo.space.listRecords', { 80 + params: { 81 + spaceUri: spaceUri as unknown as import('@atcute/lexicons').ResourceUri, 82 + collection: 'community.lexicon.calendar.event' 83 + } 84 + }), 85 + client.get('rsvp.atmo.space.listRecords', { 86 + params: { 87 + spaceUri: spaceUri as unknown as import('@atcute/lexicons').ResourceUri, 88 + collection: 'community.lexicon.calendar.rsvp' 89 + } 90 + }) 91 + ]); 92 + const events = eventsRes.ok ? eventsRes.data.records : []; 93 + const rsvps = rsvpsRes.ok ? rsvpsRes.data.records : []; 94 + 95 + return { ok: true as const, space: spaceRes.data.space, events, rsvps }; 96 + }); 97 + 98 + // ── Members ─────────────────────────────────────────────────────── 99 + 100 + export const listMembers = query(v.object({ spaceUri: atUriSchema }), async ({ spaceUri }) => { 101 + const { client } = getClient(); 102 + const res = await client.get('rsvp.atmo.space.listMembers', { 103 + params: { spaceUri: spaceUri as unknown as import('@atcute/lexicons').ResourceUri } 104 + }); 105 + if (!res.ok) error(res.status, 'listMembers failed'); 106 + return res.data.members; 107 + }); 108 + 109 + export const addMember = command( 110 + v.object({ 111 + spaceUri: atUriSchema, 112 + did: didSchema, 113 + perms: v.optional(v.string()) 114 + }), 115 + async (input) => { 116 + const { client } = getClient(); 117 + const res = await client.post('rsvp.atmo.space.admin.addMember', { 118 + input: { 119 + spaceUri: input.spaceUri as unknown as import('@atcute/lexicons').ResourceUri, 120 + did: input.did as `did:${string}:${string}`, 121 + perms: input.perms 122 + } 123 + }); 124 + if (!res.ok) error(res.status, 'addMember failed'); 125 + return { ok: true }; 126 + } 127 + ); 128 + 129 + export const removeMember = command( 130 + v.object({ 131 + spaceUri: atUriSchema, 132 + did: didSchema 133 + }), 134 + async (input) => { 135 + const { client } = getClient(); 136 + const res = await client.post('rsvp.atmo.space.admin.removeMember', { 137 + input: { 138 + spaceUri: input.spaceUri as unknown as import('@atcute/lexicons').ResourceUri, 139 + did: input.did as `did:${string}:${string}` 140 + } 141 + }); 142 + if (!res.ok) error(res.status, 'removeMember failed'); 143 + return { ok: true }; 144 + } 145 + ); 146 + 147 + // ── Invites ─────────────────────────────────────────────────────── 148 + 149 + export const createInvite = command( 150 + v.object({ 151 + spaceUri: atUriSchema, 152 + perms: v.optional(v.string()), 153 + expiresAt: v.optional(v.number()), 154 + maxUses: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))), 155 + note: v.optional(v.string()) 156 + }), 157 + async (input) => { 158 + const { client } = getClient(); 159 + const res = await client.post('rsvp.atmo.space.invite.create', { 160 + input: { ...input, spaceUri: input.spaceUri as unknown as import('@atcute/lexicons').ResourceUri } 161 + }); 162 + if (!res.ok) error(res.status, 'createInvite failed'); 163 + return res.data; 164 + } 165 + ); 166 + 167 + export const listInvites = query( 168 + v.object({ spaceUri: atUriSchema, includeRevoked: v.optional(v.boolean()) }), 169 + async ({ spaceUri, includeRevoked }) => { 170 + const { client } = getClient(); 171 + const res = await client.get('rsvp.atmo.space.invite.list', { 172 + params: { 173 + spaceUri: spaceUri as unknown as import('@atcute/lexicons').ResourceUri, 174 + includeRevoked: includeRevoked ?? false 175 + } 176 + }); 177 + if (!res.ok) error(res.status, 'listInvites failed'); 178 + return res.data.invites; 179 + } 180 + ); 181 + 182 + export const revokeInvite = command( 183 + v.object({ spaceUri: atUriSchema, tokenHash: v.string() }), 184 + async (input) => { 185 + const { client } = getClient(); 186 + const res = await client.post('rsvp.atmo.space.invite.revoke', { 187 + input: { ...input, spaceUri: input.spaceUri as unknown as import('@atcute/lexicons').ResourceUri } 188 + }); 189 + if (!res.ok) error(res.status, 'revokeInvite failed'); 190 + return res.data; 191 + } 192 + ); 193 + 194 + // ── Generic space record ops (for inside-space writes like private RSVPs) ── 195 + 196 + export const putSpaceRecord = command( 197 + v.object({ 198 + spaceUri: atUriSchema, 199 + collection: nsidSchema, 200 + rkey: v.optional(v.string()), 201 + record: v.record(v.string(), v.unknown()) 202 + }), 203 + async (input) => { 204 + const { client } = getClient(); 205 + const res = await client.post('rsvp.atmo.space.putRecord', { 206 + input: { 207 + spaceUri: input.spaceUri as unknown as import('@atcute/lexicons').ResourceUri, 208 + collection: input.collection as `${string}.${string}.${string}`, 209 + rkey: input.rkey, 210 + record: input.record 211 + } 212 + }); 213 + if (!res.ok) error(res.status, `putSpaceRecord failed`); 214 + return res.data; 215 + } 216 + ); 217 + 218 + export const deleteSpaceRecord = command( 219 + v.object({ 220 + spaceUri: atUriSchema, 221 + collection: nsidSchema, 222 + rkey: v.string() 223 + }), 224 + async (input) => { 225 + const { client } = getClient(); 226 + const res = await client.post('rsvp.atmo.space.deleteRecord', { 227 + input: { 228 + spaceUri: input.spaceUri as unknown as import('@atcute/lexicons').ResourceUri, 229 + collection: input.collection as `${string}.${string}.${string}`, 230 + rkey: input.rkey 231 + } 232 + }); 233 + if (!res.ok) error(res.status, `deleteSpaceRecord failed`); 234 + return res.data; 235 + } 236 + ); 237 + 238 + export const redeemInvite = command(v.object({ token: v.string() }), async ({ token }) => { 239 + const { client } = getClient(); 240 + const res = await client.post('rsvp.atmo.space.invite.redeem', { input: { token } }); 241 + if (!res.ok) { 242 + console.error('[redeemInvite] xrpc error', res.status, res.data); 243 + error(res.status, `redeemInvite ${res.status}: ${JSON.stringify(res.data)}`); 244 + } 245 + return res.data; 246 + });
+2 -2
src/lib/spaces/tunnel-service.generated.ts
··· 2 2 * When the tunnel is running, this file is rewritten with the tunnel's 3 3 * service DID + URL; when the tunnel stops, it is reset to null values. */ 4 4 5 - export const SERVICE_DID: string | null = "did:web:scroll-heat-flowers-software.trycloudflare.com#event_space"; 6 - export const SERVICE_URL: string | null = "https://scroll-heat-flowers-software.trycloudflare.com"; 5 + export const SERVICE_DID: string | null = "did:web:kid-retention-reservation-proc.trycloudflare.com"; 6 + export const SERVICE_URL: string | null = "https://kid-retention-reservation-proc.trycloudflare.com";
+10 -2
src/routes/(app)/calendar/+page.server.ts
··· 4 4 getServerClient, 5 5 listEventRecordsFromContrail 6 6 } from '$lib/contrail'; 7 + import { getSpacesClient } from '$lib/spaces/server/client'; 8 + import { spacesAvailable } from '$lib/spaces/config'; 7 9 import type { PageServerLoad } from './$types'; 8 10 9 11 export const load: PageServerLoad = async ({ locals, platform }) => { 10 - const client = getServerClient(platform!.env.DB); 11 12 if (!locals.did) { 12 13 return { events: [], loggedIn: false }; 13 14 } 15 + // Authenticated + spaces configured → use the service-auth client so the 16 + // server unions public events with events from every space the user is in. 17 + // Falls back to the unauthenticated client otherwise (public-only). 18 + const client = 19 + locals.client && spacesAvailable() 20 + ? getSpacesClient(locals.client, platform!.env.DB) 21 + : getServerClient(platform!.env.DB); 14 22 15 23 const now = new Date().toISOString(); 16 24 17 25 const [rsvpResponse, hostingResponse] = await Promise.all([ 18 - client.get('community.lexicon.calendar.rsvp.listRecords', { 26 + client.get('rsvp.atmo.rsvp.listRecords', { 19 27 params: { actor: locals.did, hydrateEvent: true, limit: 100 } 20 28 }), 21 29 listEventRecordsFromContrail(client, {
+24 -2
src/routes/(app)/create/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import EventEditor from '$lib/components/EventEditor.svelte'; 3 + import { page } from '$app/state'; 4 + import { goto } from '$app/navigation'; 3 5 4 6 let { data } = $props(); 7 + let privateMode = $derived(page.url.searchParams.get('private') === '1'); 8 + 9 + function toggle() { 10 + const u = new URL(page.url); 11 + if (privateMode) u.searchParams.delete('private'); 12 + else u.searchParams.set('private', '1'); 13 + goto(u.pathname + u.search, { replaceState: true }); 14 + } 5 15 </script> 6 16 7 17 <svelte:head> 8 - <title>Create Event</title> 18 + <title>{privateMode ? 'Create Private Event' : 'Create Event'}</title> 9 19 </svelte:head> 10 20 11 - <EventEditor eventData={null} actorDid={data.actorDid} rkey={data.rkey} /> 21 + <div class="mx-auto max-w-3xl px-4 pt-4"> 22 + <label class="flex cursor-pointer items-center gap-3 rounded-md border border-dashed border-base-300 bg-base-50 p-3 text-sm dark:border-base-700 dark:bg-base-900"> 23 + <input type="checkbox" checked={privateMode} onchange={toggle} class="size-4" /> 24 + <div> 25 + <div class="font-medium">Private event</div> 26 + <div class="text-xs text-base-500 dark:text-base-400"> 27 + Only people you add (or who redeem an invite link) can see the event. Not published to your public profile. 28 + </div> 29 + </div> 30 + </label> 31 + </div> 32 + 33 + <EventEditor eventData={null} actorDid={data.actorDid} rkey={data.rkey} {privateMode} />
+11 -2
src/routes/(app)/p/[actor]/+page.server.ts
··· 6 6 listAttendingEventsFromContrail, 7 7 listEventRecordsFromContrail 8 8 } from '$lib/contrail'; 9 + import { getSpacesClient } from '$lib/spaces/server/client'; 10 + import { spacesAvailable } from '$lib/spaces/config'; 9 11 import { isActorIdentifier } from '@atcute/lexicons/syntax'; 10 12 import { error } from '@sveltejs/kit'; 11 13 12 14 const PREVIEW_LIMIT = 6; 13 15 14 - export async function load({ params, platform }) { 15 - const client = getServerClient(platform!.env.DB); 16 + export async function load({ params, platform, locals }) { 17 + // Authenticated viewer + spaces configured → service-auth client so contrail 18 + // unions public events with private events from spaces the viewer is in. 19 + // Profile pages show another user's events; the viewer only sees the private 20 + // ones where *they* are a member (filtered server-side by caller DID). 21 + const client = 22 + locals.client && locals.did && spacesAvailable() 23 + ? getSpacesClient(locals.client, platform!.env.DB) 24 + : getServerClient(platform!.env.DB); 16 25 if (!isActorIdentifier(params.actor)) return; 17 26 18 27 const actor = params.actor;
+1 -1
src/routes/(app)/p/[actor]/calendar.ics/+server.ts
··· 26 26 27 27 const actorId = did as ActorIdentifier; 28 28 const [rsvpResponse, hostingResponse] = await Promise.all([ 29 - client.get('community.lexicon.calendar.rsvp.listRecords', { 29 + client.get('rsvp.atmo.rsvp.listRecords', { 30 30 params: { actor: actorId, hydrateEvent: true, limit: 100 } 31 31 }), 32 32 listEventRecordsFromContrail(client, {
+2 -700
src/routes/(app)/p/[actor]/e/[rkey]/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { isEventOngoing, type FlatEventRecord } from '$lib/contrail'; 3 - import { getCDNImageBlobUrl } from '$lib/atproto'; 4 - import { user } from '$lib/atproto/auth.svelte'; 5 - import { Avatar as FoxAvatar, Badge, Button } from '@foxui/core'; 6 - import Map from '$lib/components/Map.svelte'; 7 - import ShareModal from '$lib/components/ShareModal.svelte'; 8 - import Avatar from 'svelte-boring-avatars'; 9 - import EventRsvp from '$lib/components/EventRsvp.svelte'; 10 - import EventCard from '$lib/components/EventCard.svelte'; 11 - import EventAttendees from './EventAttendees.svelte'; 12 - import VodPlayer, { type VodPlayerApi } from '$lib/components/VodPlayer.svelte'; 13 - import VodTranscript from '$lib/components/VodTranscript.svelte'; 14 - import { page } from '$app/state'; 15 - import { marked } from 'marked'; 16 - import { sanitize } from '$lib/cal/sanitize'; 17 - import { generateICalEvent } from '$lib/cal/ical'; 18 - import { launchConfetti } from '@foxui/visual'; 19 - import ThemeBackground from '$lib/components/ThemeBackground.svelte'; 20 - import ThemeApply from '$lib/components/ThemeApply.svelte'; 21 - import { defaultTheme, type EventTheme } from '$lib/theme'; 22 - 2 + import EventView from '$lib/components/EventView.svelte'; 23 3 let { data } = $props(); 24 - 25 - let eventData: FlatEventRecord = $derived(data.eventData); 26 - let did: string = $derived(data.actorDid); 27 - let rkey: string = $derived(data.rkey); 28 - let hostProfile = $derived(data.hostProfile); 29 - let attendees = $derived(data.attendees); 30 - 31 - let theme: EventTheme = $derived(eventData.theme ?? defaultTheme); 32 - 33 - 34 - let hostUrl = $derived(`/p/${hostProfile?.handle || did}`); 35 - let eventPath = $derived(`/p/${hostProfile?.handle || did}/e/${data.rkey}`); 36 - let shareUrl = $derived( 37 - typeof window !== 'undefined' ? `${window.location.origin}${eventPath}` : eventPath 38 - ); 39 - 40 - // Times are always rendered in the viewer's local timezone — the stored UTC 41 - // instant is what the Date constructor parses, and toLocaleString/Time uses 42 - // the browser's zone by default. 43 - let startDate = $derived(new Date(eventData.startsAt)); 44 - let endDate = $derived(eventData.endsAt ? new Date(eventData.endsAt) : null); 45 - 46 - function formatMonth(date: Date): string { 47 - return date.toLocaleDateString('en-US', { month: 'short' }).toUpperCase(); 48 - } 49 - 50 - function formatDay(date: Date): number { 51 - return date.getDate(); 52 - } 53 - 54 - function formatWeekday(date: Date): string { 55 - return date.toLocaleDateString('en-US', { weekday: 'long' }); 56 - } 57 - 58 - function formatFullDate(date: Date): string { 59 - const options: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric' }; 60 - if (date.getFullYear() !== new Date().getFullYear()) { 61 - options.year = 'numeric'; 62 - } 63 - return date.toLocaleDateString('en-US', options); 64 - } 65 - 66 - function formatTime(date: Date): string { 67 - return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); 68 - } 69 - 70 - function getModeLabel(mode: string): string { 71 - if (mode.includes('virtual')) return 'Virtual'; 72 - if (mode.includes('hybrid')) return 'Hybrid'; 73 - if (mode.includes('inperson')) return 'In-Person'; 74 - return 'Event'; 75 - } 76 - 77 - function getModeColor(mode: string): 'cyan' | 'purple' | 'amber' | 'secondary' { 78 - if (mode.includes('virtual')) return 'cyan'; 79 - if (mode.includes('hybrid')) return 'purple'; 80 - if (mode.includes('inperson')) return 'amber'; 81 - return 'secondary'; 82 - } 83 - 84 - function getLocationData(locations: FlatEventRecord['locations']) { 85 - if (!locations || locations.length === 0) return null; 86 - 87 - const loc = locations.find((v) => v.$type === 'community.lexicon.location.address') as 88 - | { name?: string; street?: string; locality?: string; region?: string; country?: string } 89 - | undefined; 90 - if (!loc) return null; 91 - 92 - const shortParts = [loc.street, loc.locality].filter(Boolean); 93 - const fullParts = [loc.street, loc.locality, loc.region, loc.country].filter(Boolean); 94 - if (fullParts.length === 0) return null; 95 - 96 - const shortAddress = shortParts.join(', '); 97 - const fullAddress = fullParts.join(', '); 98 - const displayName = loc.name || undefined; 99 - const fullString = displayName ? `${displayName}, ${fullAddress}` : fullAddress; 100 - const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(fullString)}`; 101 - 102 - return { name: displayName, shortAddress, fullAddress, fullString, mapsUrl }; 103 - } 104 - 105 - let locationData = $derived(getLocationData(eventData.locations)); 106 - let location = $derived(locationData?.fullString); 107 - 108 - let geoLocation: { lat: number; lng: number } | null = $state(null); 109 - 110 - function initGeoLocation() { 111 - if (!eventData.locations || eventData.locations.length === 0) return; 112 - 113 - // Check for explicit geo coordinates first 114 - const geo = eventData.locations.find((v) => v.$type === 'community.lexicon.location.geo') as 115 - | { latitude?: string; longitude?: string } 116 - | undefined; 117 - if (geo?.latitude && geo?.longitude) { 118 - const lat = parseFloat(geo.latitude); 119 - const lng = parseFloat(geo.longitude); 120 - if (!isNaN(lat) && !isNaN(lng)) { 121 - geoLocation = { lat, lng }; 122 - return; 123 - } 124 - } 125 - 126 - // Geocode from address if available 127 - const addressQuery = locationData?.fullAddress; 128 - if (addressQuery) { 129 - fetch(`/api/geocoding?q=${encodeURIComponent(addressQuery)}`) 130 - .then((r) => (r.ok ? r.json() : null)) 131 - .then((data: unknown) => { 132 - const d = data as Record<string, unknown> | null; 133 - if (!d) return; 134 - if (d.lat && d.lon) { 135 - geoLocation = { lat: parseFloat(d.lat as string), lng: parseFloat(d.lon as string) }; 136 - } 137 - }) 138 - .catch(() => {}); 139 - } 140 - } 141 - 142 - let showShareModal = $state(false); 143 - let shareModalTitle = $state('Event created!'); 144 - let shareModalText: string | undefined = $state(undefined); 145 - 146 - import { onMount } from 'svelte'; 147 - onMount(() => { 148 - initGeoLocation(); 149 - 150 - const url = new URL(window.location.href); 151 - if (url.searchParams.has('created')) { 152 - url.searchParams.delete('created'); 153 - history.replaceState({}, '', url.pathname); 154 - launchConfetti(); 155 - shareModalTitle = 'Event created!'; 156 - shareModalText = `I'm hosting "${eventData.name}"!\n\n${shareUrl}`; 157 - showShareModal = true; 158 - } 159 - }); 160 - 161 - let thumbnailImage = $derived.by(() => { 162 - if (!eventData.media || eventData.media.length === 0) return null; 163 - const media = eventData.media.find((m) => m.role === 'thumbnail'); 164 - if (!media?.content) return null; 165 - const url = getCDNImageBlobUrl({ did, blob: media.content }); 166 - if (!url) return null; 167 - return { url, alt: media.alt || eventData.name }; 168 - }); 169 - 170 - let bannerImage = $derived.by(() => { 171 - if (!eventData.media || eventData.media.length === 0) return null; 172 - const media = eventData.media.find((m) => m.role === 'header'); 173 - if (!media?.content) return null; 174 - const url = getCDNImageBlobUrl({ did, blob: media.content }); 175 - if (!url) return null; 176 - return { url, alt: media.alt || eventData.name }; 177 - }); 178 - 179 - // Prefer thumbnail; fall back to header/banner image 180 - let displayImage = $derived(thumbnailImage ?? bannerImage); 181 - let isBannerOnly = $derived(!thumbnailImage && !!bannerImage); 182 - 183 - let isSameDay = $derived( 184 - endDate && 185 - startDate.getFullYear() === endDate.getFullYear() && 186 - startDate.getMonth() === endDate.getMonth() && 187 - startDate.getDate() === endDate.getDate() 188 - ); 189 - 190 - let isOngoing = $derived(isEventOngoing(eventData.startsAt, eventData.endsAt)); 191 - let isPast = $derived(endDate ? endDate < new Date() : false); 192 - 193 - const renderer = new marked.Renderer(); 194 - renderer.link = ({ href, text }) => 195 - `<a target="_blank" rel="noopener noreferrer nofollow" href="${href}" class="text-accent-600 dark:text-accent-400 hover:underline">${text}</a>`; 196 - 197 - function renderDescription( 198 - text: string, 199 - facets?: { 200 - index: { byteStart: number; byteEnd: number }; 201 - features: { $type: string; did?: string; uri?: string; tag?: string }[]; 202 - }[] 203 - ): string { 204 - let result = text; 205 - 206 - if (facets && facets.length > 0) { 207 - const encoder = new TextEncoder(); 208 - const encoded = encoder.encode(text); 209 - const decoder = new TextDecoder(); 210 - 211 - // Sort facets in reverse order by byteStart so replacements don't shift positions 212 - const sorted = [...facets].sort((a, b) => b.index.byteStart - a.index.byteStart); 213 - 214 - for (const facet of sorted) { 215 - const feature = facet.features?.[0]; 216 - if (!feature) continue; 217 - 218 - const segmentBytes = encoded.slice(facet.index.byteStart, facet.index.byteEnd); 219 - const segmentText = decoder.decode(segmentBytes); 220 - 221 - let mdLink: string | null = null; 222 - switch (feature.$type) { 223 - case 'app.bsky.richtext.facet#mention': 224 - mdLink = `[${segmentText}](/${feature.did})`; 225 - break; 226 - case 'app.bsky.richtext.facet#link': 227 - mdLink = `[${segmentText}](${feature.uri})`; 228 - break; 229 - case 'app.bsky.richtext.facet#tag': 230 - mdLink = `[${segmentText}](https://bsky.app/hashtag/${feature.tag})`; 231 - break; 232 - } 233 - 234 - if (mdLink) { 235 - // Convert byte offsets to character offsets for string replacement 236 - const before = decoder.decode(encoded.slice(0, facet.index.byteStart)); 237 - const after = decoder.decode(encoded.slice(facet.index.byteEnd)); 238 - result = before + mdLink + after; 239 - } 240 - } 241 - } 242 - 243 - return marked.parse(result, { renderer }) as string; 244 - } 245 - 246 - let descriptionHtml = $derived( 247 - eventData.description 248 - ? sanitize( 249 - renderDescription( 250 - eventData.description, 251 - eventData.facets as 252 - | { 253 - index: { byteStart: number; byteEnd: number }; 254 - features: { $type: string; did?: string; uri?: string; tag?: string }[]; 255 - }[] 256 - | undefined 257 - ), 258 - { ADD_ATTR: ['target'] } 259 - ) 260 - : null 261 - ); 262 - 263 - let eventUri = $derived(`at://${did}/community.lexicon.calendar.event/${rkey}`); 264 - 265 - let ogImageUrl = $derived(`${page.url.origin}${page.url.pathname}/og.png`); 266 - 267 - let isOwner = $derived(user.isLoggedIn && user.did === did); 268 - 269 - let speakers = $derived(data.speakerProfiles ?? []); 270 - 271 - let vodCurrentTime = $state(0); 272 - let vodApi: VodPlayerApi | undefined = $state(); 273 - 274 - let attendeesRef: EventAttendees | undefined = $state(); 275 - 276 - function handleRsvp(status: 'going' | 'interested') { 277 - if (!user.did) return; 278 - attendeesRef?.addAttendee({ 279 - did: user.did, 280 - status, 281 - avatar: user.profile?.avatar, 282 - name: user.profile?.displayName || user.profile?.handle || user.did, 283 - handle: user.profile?.handle, 284 - url: `/${user.profile?.handle || user.did}` 285 - }); 286 - if (status === 'interested') return; 287 - shareModalTitle = "You're going!"; 288 - shareModalText = `I'm going to "${eventData.name}".\n\n${shareUrl}`; 289 - showShareModal = true; 290 - } 291 - 292 - function handleRsvpCancel() { 293 - if (!user.did) return; 294 - attendeesRef?.removeAttendee(user.did); 295 - } 296 - 297 - function downloadIcs() { 298 - const ical = generateICalEvent(eventData, eventUri, page.url.href); 299 - const blob = new Blob([ical], { type: 'text/calendar;charset=utf-8' }); 300 - const url = URL.createObjectURL(blob); 301 - const a = document.createElement('a'); 302 - a.href = url; 303 - a.download = `${eventData.name.replace(/[^a-zA-Z0-9]/g, '-')}.ics`; 304 - a.click(); 305 - URL.revokeObjectURL(url); 306 - } 307 4 </script> 308 5 309 - <svelte:head> 310 - <title>{eventData.name}</title> 311 - <meta name="description" content={eventData.description || `Event: ${eventData.name}`} /> 312 - <meta property="og:title" content={eventData.name} /> 313 - <meta property="og:description" content={eventData.description || `Event: ${eventData.name}`} /> 314 - <meta name="twitter:card" content="summary_large_image" /> 315 - <meta name="twitter:title" content={eventData.name} /> 316 - <meta name="twitter:description" content={eventData.description || `Event: ${eventData.name}`} /> 317 - <meta name="twitter:image" content={ogImageUrl} /> 318 - </svelte:head> 319 - 320 - <ThemeApply accentColor={theme.accentColor} baseColor={theme.baseColor} /> 321 - <ThemeBackground {theme} /> 322 - 323 - <div class="min-h-screen px-6 py-12 sm:py-12"> 324 - <div class="mx-auto max-w-3xl"> 325 - <!-- Banner image (full width, only when no thumbnail) --> 326 - {#if isBannerOnly && displayImage} 327 - <img 328 - src={displayImage.url} 329 - alt={displayImage.alt} 330 - class="border-base-200 dark:border-base-800 mb-8 aspect-3/1 w-full rounded-2xl border object-cover" 331 - /> 332 - {/if} 333 - 334 - <!-- Two-column layout: image left, details right --> 335 - <div 336 - class="grid grid-cols-1 gap-8 md:grid-cols-[14rem_1fr] md:grid-rows-[auto_1fr] md:gap-x-10 md:gap-y-6 lg:grid-cols-[16rem_1fr]" 337 - > 338 - <!-- Thumbnail image (left column) --> 339 - {#if !isBannerOnly} 340 - <div class="order-1 max-w-sm md:order-0 md:col-start-1 md:max-w-none"> 341 - {#if displayImage} 342 - <img 343 - src={displayImage.url} 344 - alt={displayImage.alt} 345 - class="border-base-200 dark:border-base-800 bg-base-200 dark:bg-base-950/50 aspect-square w-full rounded-2xl border object-cover" 346 - /> 347 - {:else} 348 - <div 349 - class="border-base-200 dark:border-base-800 aspect-square w-full overflow-hidden rounded-2xl border [&>svg]:h-full [&>svg]:w-full" 350 - > 351 - <Avatar 352 - size={256} 353 - name={data.rkey} 354 - variant="marble" 355 - colors={['#92A1C6', '#146A7C', '#F0AB3D', '#C271B4', '#C20D90']} 356 - square 357 - /> 358 - </div> 359 - {/if} 360 - {#if isOwner} 361 - <Button href="./{rkey}/edit" class="mt-9 w-full">Edit Event</Button> 362 - {/if} 363 - </div> 364 - {/if} 365 - 366 - <!-- Right column: event details --> 367 - <div class="order-2 min-w-0 md:order-0 md:col-start-2 md:row-span-2 md:row-start-1"> 368 - <div class="mb-2"> 369 - <h1 class="text-base-900 dark:text-base-50 text-3xl leading-tight font-bold sm:text-4xl"> 370 - {eventData.name} 371 - </h1> 372 - </div> 373 - 374 - <!-- Badges --> 375 - {#if eventData.mode || isOngoing} 376 - <div class="mb-8 flex items-center gap-2"> 377 - {#if isOngoing} 378 - <Badge size="md" variant="primary"> 379 - <span class="bg-accent-500 mr-1 inline-block size-1.5 animate-pulse rounded-full" 380 - ></span> 381 - Live 382 - </Badge> 383 - {/if} 384 - {#if eventData.mode} 385 - <Badge size="md" variant={getModeColor(eventData.mode)} 386 - >{getModeLabel(eventData.mode)}</Badge 387 - > 388 - {/if} 389 - </div> 390 - {/if} 391 - 392 - <!-- Date row --> 393 - <div class="mb-4 flex items-center gap-4"> 394 - <div 395 - class="border-base-200 dark:border-base-700 bg-base-100 dark:bg-base-950/30 flex size-12 shrink-0 flex-col items-center justify-center overflow-hidden rounded-xl border" 396 - > 397 - <span class="text-base-500 dark:text-base-400 text-[9px] leading-none font-semibold"> 398 - {formatMonth(startDate)} 399 - </span> 400 - <span class="text-base-900 dark:text-base-50 text-lg leading-tight font-bold"> 401 - {formatDay(startDate)} 402 - </span> 403 - </div> 404 - <div> 405 - <p class="text-base-900 dark:text-base-50 font-semibold"> 406 - {formatWeekday(startDate)}, {formatFullDate(startDate)} 407 - {#if endDate && !isSameDay} 408 - - {formatWeekday(endDate)}, {formatFullDate(endDate)} 409 - {/if} 410 - </p> 411 - <p class="text-base-500 dark:text-base-400 text-sm"> 412 - {formatTime(startDate)} 413 - {#if endDate && isSameDay} 414 - - {formatTime(endDate)} 415 - {/if} 416 - </p> 417 - </div> 418 - </div> 419 - 420 - <!-- Location row --> 421 - {#if locationData} 422 - <a 423 - href={locationData.mapsUrl} 424 - target="_blank" 425 - rel="noopener noreferrer" 426 - class="mb-6 flex items-center gap-4 transition-opacity hover:opacity-80" 427 - > 428 - <div 429 - class="border-base-200 dark:border-base-700 bg-base-100 dark:bg-base-950/30 flex size-12 shrink-0 items-center justify-center rounded-xl border" 430 - > 431 - <svg 432 - xmlns="http://www.w3.org/2000/svg" 433 - fill="none" 434 - viewBox="0 0 24 24" 435 - stroke-width="1.5" 436 - stroke="currentColor" 437 - class="text-base-900 dark:text-base-200 size-5" 438 - > 439 - <path 440 - stroke-linecap="round" 441 - stroke-linejoin="round" 442 - d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 443 - /> 444 - <path 445 - stroke-linecap="round" 446 - stroke-linejoin="round" 447 - d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z" 448 - /> 449 - </svg> 450 - </div> 451 - <div> 452 - {#if locationData.name} 453 - <p class="text-base-900 dark:text-base-50 font-semibold">{locationData.name}</p> 454 - <p class="text-base-500 dark:text-base-400 text-sm">{locationData.shortAddress}</p> 455 - {:else} 456 - <p class="text-base-900 dark:text-base-50 font-semibold"> 457 - {locationData.shortAddress} 458 - </p> 459 - {/if} 460 - </div> 461 - </a> 462 - {/if} 463 - 464 - <!-- Part of --> 465 - {#if data.parentEvent} 466 - <div 467 - class="border-base-200 dark:border-base-800 bg-base-100 dark:bg-base-950/50 mt-8 mb-2 justify-center rounded-2xl border p-4" 468 - > 469 - <p 470 - class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 471 - > 472 - Part of 473 - </p> 474 - <EventCard event={data.parentEvent} actor="atprotocol.dev" /> 475 - <Button href="/p/atmosphereconf.org" size="lg" class="mt-6 w-full"> 476 - See full schedule 477 - </Button> 478 - </div> 479 - {/if} 480 - 481 - {#if did === 'did:plc:lehcqqkwzcwvjvw66uthu5oq' && rkey === '3lte3c7x43l2e'} 482 - <Button href="/p/atmosphereconf.org" size="lg" class="mb-4 w-full"> 483 - See full schedule 484 - </Button> 485 - {/if} 486 - 487 - {#if !isPast} 488 - <EventRsvp 489 - {eventUri} 490 - eventCid={eventData.cid ?? null} 491 - initialRsvpStatus={data.viewerRsvpStatus} 492 - initialRsvpRkey={data.viewerRsvpRkey} 493 - onrsvp={handleRsvp} 494 - oncancel={handleRsvpCancel} 495 - /> 496 - {/if} 497 - 498 - <!-- About Event --> 499 - {#if descriptionHtml} 500 - <div class="mt-8 mb-8"> 501 - <p 502 - class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 503 - > 504 - About 505 - </p> 506 - <div 507 - class="text-base-700 dark:text-base-300 prose dark:prose-invert prose-a:text-accent-600 dark:prose-a:text-accent-400 prose-a:hover:underline prose-a:no-underline max-w-none leading-relaxed wrap-break-word" 508 - > 509 - {@html descriptionHtml} 510 - </div> 511 - </div> 512 - {/if} 513 - 514 - <!-- Recording --> 515 - {#if data.vod} 516 - <div class="mt-8 mb-8"> 517 - <p 518 - class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 519 - > 520 - Recording 521 - </p> 522 - <VodPlayer 523 - playlistUrl={data.vod.playlistUrl} 524 - title={eventData.name} 525 - subtitlesUrl="/vods/{rkey}-karaoke.vtt" 526 - bind:currentTime={vodCurrentTime} 527 - bind:api={vodApi} 528 - /> 529 - </div> 530 - 531 - <!-- <div class="mt-4 mb-8"> 532 - <p 533 - class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 534 - > 535 - Transcript 536 - </p> 537 - <VodTranscript 538 - transcriptUrl="/vods/{rkey}.json" 539 - currentTime={vodCurrentTime} 540 - onseek={(time) => vodApi?.seek(time)} 541 - /> 542 - </div> --> 543 - {/if} 544 - 545 - <!-- Map --> 546 - {#if geoLocation && locationData} 547 - <div class="mt-8 mb-8"> 548 - <p 549 - class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 550 - > 551 - Location 552 - </p> 553 - <a 554 - href={locationData.mapsUrl} 555 - target="_blank" 556 - rel="noopener noreferrer" 557 - class="block transition-opacity hover:opacity-80" 558 - > 559 - <div class="h-64 w-full overflow-hidden rounded-xl"> 560 - <Map lat={geoLocation.lat} lng={geoLocation.lng} /> 561 - </div> 562 - <p class="text-base-500 dark:text-base-400 mt-2 text-sm"> 563 - {locationData.fullString} 564 - </p> 565 - </a> 566 - </div> 567 - {/if} 568 - </div> 569 - 570 - <!-- Left column: sidebar info --> 571 - <div class="order-3 space-y-6 md:order-0 md:col-start-1"> 572 - <!-- Hosted By --> 573 - <div> 574 - <p 575 - class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 576 - > 577 - Hosted By 578 - </p> 579 - <a 580 - href={hostUrl} 581 - class="text-base-900 dark:text-base-100 flex items-center gap-2.5 font-medium transition-opacity hover:opacity-80" 582 - > 583 - <FoxAvatar 584 - src={hostProfile?.avatar} 585 - alt={hostProfile?.displayName || hostProfile?.handle || did} 586 - class="size-8 shrink-0" 587 - /> 588 - <span class="truncate text-sm"> 589 - {hostProfile?.displayName || hostProfile?.handle || did} 590 - </span> 591 - </a> 592 - </div> 593 - 594 - <!-- Speakers --> 595 - {#if speakers.length > 0} 596 - <div> 597 - <p 598 - class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 599 - > 600 - Speakers 601 - </p> 602 - <div class="space-y-2"> 603 - {#each speakers as speaker, i (speaker.id || i)} 604 - {#if speaker.handle} 605 - <a 606 - href="/p/{speaker.handle}" 607 - class="text-base-900 dark:text-base-100 flex items-center gap-2.5 font-medium transition-opacity hover:opacity-80" 608 - > 609 - <FoxAvatar src={speaker.avatar} alt={speaker.name} class="size-8 shrink-0" /> 610 - <span class="truncate text-sm">{speaker.name}</span> 611 - </a> 612 - {:else} 613 - <div 614 - class="text-base-900 dark:text-base-100 flex items-center gap-2.5 font-medium" 615 - > 616 - <FoxAvatar alt={speaker.name} class="size-8 shrink-0" /> 617 - <span class="truncate text-sm">{speaker.name}</span> 618 - </div> 619 - {/if} 620 - {/each} 621 - </div> 622 - </div> 623 - {/if} 624 - 625 - {#if eventData.uris && eventData.uris.length > 0} 626 - <!-- Links --> 627 - <div> 628 - <p 629 - class="text-base-500 dark:text-base-400 mb-4 text-xs font-semibold tracking-wider uppercase" 630 - > 631 - Links 632 - </p> 633 - <div class="space-y-3"> 634 - {#each eventData.uris as link (link.name + link.uri)} 635 - <a 636 - href={link.uri} 637 - target="_blank" 638 - rel="noopener noreferrer" 639 - class="text-base-700 dark:text-base-300 hover:text-base-900 dark:hover:text-base-100 flex items-center gap-1.5 text-sm transition-colors" 640 - > 641 - <svg 642 - xmlns="http://www.w3.org/2000/svg" 643 - fill="none" 644 - viewBox="0 0 24 24" 645 - stroke-width="1.5" 646 - stroke="currentColor" 647 - class="size-3.5 shrink-0" 648 - > 649 - <path 650 - stroke-linecap="round" 651 - stroke-linejoin="round" 652 - d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 653 - /> 654 - </svg> 655 - <span class="truncate">{link.name || link.uri.replace(/^https?:\/\//, '')}</span> 656 - </a> 657 - {/each} 658 - </div> 659 - </div> 660 - {/if} 661 - 662 - <!-- Add to Calendar --> 663 - <button 664 - onclick={downloadIcs} 665 - class="text-base-700 dark:text-base-300 hover:text-base-900 dark:hover:text-base-100 flex cursor-pointer items-center gap-2 text-sm font-medium transition-colors" 666 - > 667 - <svg 668 - xmlns="http://www.w3.org/2000/svg" 669 - fill="none" 670 - viewBox="0 0 24 24" 671 - stroke-width="1.5" 672 - stroke="currentColor" 673 - class="size-4" 674 - > 675 - <path 676 - stroke-linecap="round" 677 - stroke-linejoin="round" 678 - d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" 679 - /> 680 - </svg> 681 - Add to Calendar 682 - </button> 683 - 684 - <!-- Attendees --> 685 - <EventAttendees 686 - bind:this={attendeesRef} 687 - going={attendees.going} 688 - interested={attendees.interested} 689 - goingCount={attendees.goingCount} 690 - interestedCount={attendees.interestedCount} 691 - /> 692 - </div> 693 - </div> 694 - </div> 695 - </div> 696 - 697 - <ShareModal 698 - bind:open={showShareModal} 699 - url={shareUrl} 700 - title={shareModalTitle} 701 - shareText={shareModalText} 702 - eventName={eventData.name} 703 - {ogImageUrl} 704 - /> 6 + <EventView {data} />
src/routes/(app)/p/[actor]/e/[rkey]/EventAttendees.svelte src/lib/components/EventAttendees.svelte
+153
src/routes/(app)/p/[actor]/e/[rkey]/s/[skey]/+page.server.ts
··· 1 + import type { PageServerLoad } from './$types'; 2 + import type { Did, ActorIdentifier } from '@atcute/lexicons'; 3 + import { getActor } from '$lib/actor'; 4 + import { 5 + flattenEventRecord, 6 + getProfileFromContrail, 7 + getProfileBlobUrl, 8 + getServerClient, 9 + type FlatEventRecord, 10 + type HostProfile 11 + } from '$lib/contrail'; 12 + import { getPrivateSpace } from '$lib/spaces/server/spaces.remote'; 13 + import { SPACE_TYPE } from '$lib/spaces/config'; 14 + 15 + export const load: PageServerLoad = async ({ params, locals, platform, url }) => { 16 + const ownerDid = await getActor(params.actor); 17 + if (!ownerDid) { 18 + return { authState: 'not-found' as const }; 19 + } 20 + 21 + if (!locals.did) { 22 + return { authState: 'anon' as const }; 23 + } 24 + 25 + const spaceUri = `at://${ownerDid}/${SPACE_TYPE}/${params.skey}`; 26 + const hasInvite = url.searchParams.has('invite'); 27 + 28 + const spaceResult = await getPrivateSpace({ spaceUri }).catch((e) => { 29 + console.error('[private-event-load] getPrivateSpace threw unexpectedly:', e); 30 + return { ok: false as const, status: 500 }; 31 + }); 32 + if (!spaceResult.ok) { 33 + if (hasInvite) return { authState: 'pending-invite' as const }; 34 + return { authState: 'no-access' as const }; 35 + } 36 + const spaceData = spaceResult; 37 + 38 + // Find the specific event this URL is for, not just the first one in the space. 39 + // Supports spaces that hold multiple events (future) without breaking existing 40 + // links when a space has a single event. 41 + const stored = spaceData.events.find((e) => e.rkey === params.rkey); 42 + if (!stored) { 43 + return { authState: 'no-access' as const }; 44 + } 45 + 46 + const synthesizedRecord = { 47 + record: stored.record as Record<string, unknown>, 48 + cid: stored.cid ?? null, 49 + did: stored.authorDid, 50 + rkey: stored.rkey, 51 + uri: `at://${stored.authorDid}/${stored.collection}/${stored.rkey}`, 52 + space: spaceUri 53 + }; 54 + const eventData = flattenEventRecord(synthesizedRecord as never) as FlatEventRecord | null; 55 + if (!eventData) { 56 + return { authState: 'no-access' as const }; 57 + } 58 + 59 + const client = getServerClient(platform!.env.DB); 60 + let hostProfile: HostProfile | null = null; 61 + try { 62 + const p = await getProfileFromContrail(client, ownerDid as ActorIdentifier); 63 + if (p) { 64 + hostProfile = { 65 + did: p.did, 66 + handle: p.handle && p.handle !== 'handle.invalid' ? p.handle : ownerDid, 67 + displayName: p.record?.displayName, 68 + avatar: p.record?.avatar ? getProfileBlobUrl(p.did, p.record.avatar) : undefined 69 + }; 70 + } 71 + } catch { 72 + // best-effort 73 + } 74 + 75 + const rsvps = (spaceData.rsvps ?? []) as Array<{ 76 + authorDid: string; 77 + rkey: string; 78 + record: { status?: string; createdAt?: string; subject?: { uri?: string } }; 79 + }>; 80 + const going: Array<{ did: string; rkey: string; createdAt?: string }> = []; 81 + const interested: Array<{ did: string; rkey: string; createdAt?: string }> = []; 82 + let viewerRsvpStatus: 'going' | 'interested' | 'notgoing' | null = null; 83 + let viewerRsvpRkey: string | null = null; 84 + 85 + for (const r of rsvps) { 86 + const statusFull = r.record?.status ?? ''; 87 + const shortStatus = statusFull.split('#')[1] as 'going' | 'interested' | 'notgoing' | undefined; 88 + if (shortStatus === 'going') 89 + going.push({ did: r.authorDid, rkey: r.rkey, createdAt: r.record?.createdAt }); 90 + else if (shortStatus === 'interested') 91 + interested.push({ did: r.authorDid, rkey: r.rkey, createdAt: r.record?.createdAt }); 92 + if (r.authorDid === locals.did && shortStatus) { 93 + viewerRsvpStatus = shortStatus; 94 + viewerRsvpRkey = r.rkey; 95 + } 96 + } 97 + 98 + const attendeeDids = Array.from(new Set([...going, ...interested].map((a) => a.did))); 99 + const profileMap = new Map<string, HostProfile>(); 100 + await Promise.all( 101 + attendeeDids.map(async (did) => { 102 + try { 103 + const p = await getProfileFromContrail(client, did as ActorIdentifier); 104 + if (p) { 105 + profileMap.set(did, { 106 + did: p.did, 107 + handle: p.handle && p.handle !== 'handle.invalid' ? p.handle : did, 108 + displayName: p.record?.displayName, 109 + avatar: p.record?.avatar ? getProfileBlobUrl(p.did, p.record.avatar) : undefined 110 + }); 111 + } 112 + } catch { 113 + /* best-effort */ 114 + } 115 + }) 116 + ); 117 + 118 + const shapeAttendee = (a: { did: string; rkey: string; createdAt?: string }) => { 119 + const p = profileMap.get(a.did); 120 + return { 121 + did: a.did, 122 + rkey: a.rkey, 123 + handle: p?.handle ?? a.did, 124 + displayName: p?.displayName, 125 + avatar: p?.avatar, 126 + createdAt: a.createdAt 127 + }; 128 + }; 129 + 130 + return { 131 + authState: 'member' as const, 132 + ownerDid: ownerDid as Did, 133 + spaceUri, 134 + spaceKey: params.skey, 135 + isOwner: ownerDid === locals.did, 136 + eventData, 137 + actorDid: ownerDid, 138 + rkey: params.rkey, 139 + hostProfile, 140 + attendees: { 141 + going: going.map(shapeAttendee), 142 + interested: interested.map(shapeAttendee), 143 + goingCount: going.length, 144 + interestedCount: interested.length 145 + }, 146 + viewerRsvpStatus, 147 + viewerRsvpRkey, 148 + parentEvent: null, 149 + vod: null, 150 + speakerProfiles: [] as Array<{ id?: string; name: string; avatar?: string; handle?: string }>, 151 + ogImage: undefined as string | undefined 152 + }; 153 + };
+79
src/routes/(app)/p/[actor]/e/[rkey]/s/[skey]/+page.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { goto, invalidateAll } from '$app/navigation'; 4 + import { page } from '$app/state'; 5 + import EventView from '$lib/components/EventView.svelte'; 6 + import { redeemInvite } from '$lib/spaces/server/spaces.remote'; 7 + import { atProtoLoginModalState } from '$lib/components/LoginModal.svelte'; 8 + 9 + let { data } = $props(); 10 + 11 + let inviteBusy = $state(false); 12 + let inviteError: string | null = $state(null); 13 + 14 + onMount(async () => { 15 + const token = page.url.searchParams.get('invite'); 16 + if (!token) return; 17 + if (data.authState === 'anon') return; 18 + 19 + inviteBusy = true; 20 + try { 21 + await redeemInvite({ token }); 22 + const u = new URL(page.url); 23 + u.searchParams.delete('invite'); 24 + await goto(u.pathname + u.search, { replaceState: true, invalidateAll: true }); 25 + } catch (e) { 26 + inviteError = e instanceof Error ? e.message : String(e); 27 + await invalidateAll(); 28 + } finally { 29 + inviteBusy = false; 30 + } 31 + }); 32 + </script> 33 + 34 + {#if data.authState === 'anon'} 35 + <div class="mx-auto max-w-md px-4 pt-16 text-center"> 36 + <h1 class="mb-2 text-xl font-semibold">Login to see event</h1> 37 + <p class="text-base-500 mb-4 text-sm"> 38 + This is a private event. Sign in with atproto to view. 39 + </p> 40 + <button 41 + class="bg-base-900 dark:bg-base-50 dark:text-base-900 rounded-md px-4 py-2 text-sm font-medium text-white" 42 + onclick={() => atProtoLoginModalState.show()} 43 + > 44 + Sign in 45 + </button> 46 + </div> 47 + {:else if data.authState === 'pending-invite'} 48 + <div class="mx-auto max-w-md px-4 pt-16 text-center"> 49 + <p class="text-base-500 text-sm">Redeeming invite…</p> 50 + {#if inviteError} 51 + <p class="mt-3 text-xs text-red-600">{inviteError}</p> 52 + {/if} 53 + </div> 54 + {:else if data.authState === 'no-access' || data.authState === 'not-found'} 55 + <div class="mx-auto max-w-md px-4 pt-16 text-center"> 56 + <h1 class="mb-2 text-xl font-semibold">Event not found</h1> 57 + <p class="text-base-500 text-sm"> 58 + This event doesn't exist, or you don't have permission to view it. 59 + </p> 60 + {#if inviteError} 61 + <p class="text-base-500 mt-3 text-xs">Invite redemption failed: {inviteError}</p> 62 + {/if} 63 + </div> 64 + {:else} 65 + {#if inviteBusy} 66 + <div class="mx-auto max-w-md px-4 pt-4 text-center"> 67 + <p class="text-base-500 text-sm">Redeeming invite…</p> 68 + </div> 69 + {/if} 70 + <EventView {data} /> 71 + {#if data.isOwner} 72 + <div class="mx-auto max-w-3xl px-4 pb-12"> 73 + <a 74 + href="/p/{data.hostProfile?.handle || data.ownerDid}/e/{data.rkey}/s/{data.spaceKey}/admin" 75 + class="text-base-500 text-sm underline">Manage members & invites →</a 76 + > 77 + </div> 78 + {/if} 79 + {/if}
+29
src/routes/(app)/p/[actor]/e/[rkey]/s/[skey]/admin/+page.server.ts
··· 1 + import { error, redirect } from '@sveltejs/kit'; 2 + import type { PageServerLoad } from './$types'; 3 + import { getActor } from '$lib/actor'; 4 + import { listMembers, listInvites } from '$lib/spaces/server/spaces.remote'; 5 + import { SPACE_TYPE } from '$lib/spaces/config'; 6 + 7 + export const load: PageServerLoad = async ({ params, locals }) => { 8 + if (!locals.did) redirect(303, '/login'); 9 + 10 + const ownerDid = await getActor(params.actor); 11 + if (!ownerDid) throw error(404, 'Not found'); 12 + if (ownerDid !== locals.did) throw error(403, 'Only the host can manage this event'); 13 + 14 + const spaceUri = `at://${ownerDid}/${SPACE_TYPE}/${params.skey}`; 15 + 16 + const [members, invites] = await Promise.all([ 17 + listMembers({ spaceUri }).catch(() => []), 18 + listInvites({ spaceUri }).catch(() => []) 19 + ]); 20 + 21 + return { 22 + spaceUri, 23 + actor: params.actor, 24 + rkey: params.rkey, 25 + spaceKey: params.skey, 26 + members, 27 + invites 28 + }; 29 + };
+216
src/routes/(app)/p/[actor]/e/[rkey]/s/[skey]/admin/+page.svelte
··· 1 + <script lang="ts"> 2 + import { invalidateAll } from '$app/navigation'; 3 + import { 4 + addMember, 5 + removeMember, 6 + createInvite, 7 + revokeInvite 8 + } from '$lib/spaces/server/spaces.remote'; 9 + 10 + let { data } = $props(); 11 + 12 + let tab: 'members' | 'invites' = $state('members'); 13 + 14 + let newMemberDid = $state(''); 15 + let newMemberPerms: 'read' | 'write' = $state('write'); 16 + let memberErr: string | null = $state(null); 17 + let memberBusy = $state(false); 18 + 19 + let inviteExpiryDays = $state(7); 20 + let inviteMaxUses = $state<number | ''>(''); 21 + let inviteNote = $state(''); 22 + let inviteBusy = $state(false); 23 + let lastInviteToken: string | null = $state(null); 24 + 25 + async function handleAddMember(e: Event) { 26 + e.preventDefault(); 27 + memberErr = null; 28 + memberBusy = true; 29 + try { 30 + await addMember({ 31 + spaceUri: data.spaceUri, 32 + did: newMemberDid.trim(), 33 + perms: newMemberPerms || undefined 34 + }); 35 + newMemberDid = ''; 36 + await invalidateAll(); 37 + } catch (err) { 38 + memberErr = (err as Error).message || 'Failed to add'; 39 + } finally { 40 + memberBusy = false; 41 + } 42 + } 43 + 44 + async function handleRemoveMember(did: string) { 45 + if (!confirm(`Remove ${did}?`)) return; 46 + await removeMember({ spaceUri: data.spaceUri, did }); 47 + await invalidateAll(); 48 + } 49 + 50 + async function handleCreateInvite(e: Event) { 51 + e.preventDefault(); 52 + inviteBusy = true; 53 + try { 54 + const result = await createInvite({ 55 + spaceUri: data.spaceUri, 56 + expiresAt: 57 + inviteExpiryDays > 0 ? Date.now() + inviteExpiryDays * 24 * 3600 * 1000 : undefined, 58 + maxUses: typeof inviteMaxUses === 'number' ? inviteMaxUses : undefined, 59 + note: inviteNote || undefined 60 + }); 61 + lastInviteToken = result.token; 62 + inviteNote = ''; 63 + await invalidateAll(); 64 + } finally { 65 + inviteBusy = false; 66 + } 67 + } 68 + 69 + async function handleRevoke(tokenHash: string) { 70 + if (!confirm('Revoke this invite?')) return; 71 + await revokeInvite({ spaceUri: data.spaceUri, tokenHash }); 72 + await invalidateAll(); 73 + } 74 + 75 + function inviteUrl(token: string) { 76 + const origin = typeof location !== 'undefined' ? location.origin : ''; 77 + return `${origin}/p/${data.actor}/e/${data.rkey}/s/${data.spaceKey}?invite=${token}`; 78 + } 79 + </script> 80 + 81 + <svelte:head> 82 + <title>Manage private event</title> 83 + </svelte:head> 84 + 85 + <div class="mx-auto max-w-3xl px-4 pt-6 pb-12"> 86 + <div class="mb-4"> 87 + <a 88 + href="/p/{data.actor}/e/{data.rkey}/s/{data.spaceKey}" 89 + class="text-base-500 text-sm">← back to event</a 90 + > 91 + </div> 92 + 93 + <h1 class="mb-4 text-2xl font-semibold">Manage private event</h1> 94 + 95 + <nav class="mb-4 flex gap-4 border-b border-base-200 dark:border-base-800"> 96 + {#each ['members', 'invites'] as t (t)} 97 + <button 98 + class:border-b-2={tab === t} 99 + class:border-base-900={tab === t} 100 + class:dark:border-base-50={tab === t} 101 + class="-mb-px pb-2 text-sm capitalize" 102 + onclick={() => (tab = t as typeof tab)} 103 + > 104 + {t} 105 + {#if t === 'members'}<span class="text-base-500 ml-1">({data.members.length})</span>{/if} 106 + {#if t === 'invites'}<span class="text-base-500 ml-1">({data.invites.length})</span>{/if} 107 + </button> 108 + {/each} 109 + </nav> 110 + 111 + {#if tab === 'members'} 112 + <form onsubmit={handleAddMember} class="mb-6 flex flex-wrap gap-2"> 113 + <input 114 + bind:value={newMemberDid} 115 + placeholder="did:plc:..." 116 + class="border-base-300 dark:border-base-700 dark:bg-base-900 flex-1 rounded-md border px-2 py-1.5 text-sm" 117 + /> 118 + <select 119 + bind:value={newMemberPerms} 120 + class="border-base-300 dark:border-base-700 dark:bg-base-900 w-28 rounded-md border px-2 py-1.5 text-sm" 121 + > 122 + <option value="write">write</option> 123 + <option value="read">read</option> 124 + </select> 125 + <button 126 + type="submit" 127 + disabled={memberBusy} 128 + class="bg-base-900 dark:bg-base-50 dark:text-base-900 rounded-md px-3 py-1.5 text-sm font-medium text-white disabled:opacity-50" 129 + >Add</button 130 + > 131 + </form> 132 + {#if memberErr} 133 + <p class="mb-3 text-sm text-red-600">{memberErr}</p> 134 + {/if} 135 + <ul class="divide-base-200 dark:divide-base-800 divide-y"> 136 + {#each data.members as m (m.did)} 137 + <li class="flex items-center justify-between py-2"> 138 + <div> 139 + <div class="font-mono text-xs">{m.did}</div> 140 + <div class="text-base-500 text-xs">{m.perms}</div> 141 + </div> 142 + <button class="text-xs text-red-600" onclick={() => handleRemoveMember(m.did)} 143 + >Remove</button 144 + > 145 + </li> 146 + {/each} 147 + </ul> 148 + {:else} 149 + <form onsubmit={handleCreateInvite} class="mb-6 space-y-2"> 150 + <div class="flex gap-2"> 151 + <label class="text-sm"> 152 + Expires in 153 + <input 154 + type="number" 155 + min="0" 156 + bind:value={inviteExpiryDays} 157 + class="border-base-300 dark:border-base-700 dark:bg-base-900 ml-1 w-16 rounded-md border px-2 py-1 text-sm" 158 + /> 159 + days (0 = never) 160 + </label> 161 + <label class="text-sm"> 162 + Max uses 163 + <input 164 + type="number" 165 + min="1" 166 + bind:value={inviteMaxUses} 167 + placeholder="∞" 168 + class="border-base-300 dark:border-base-700 dark:bg-base-900 ml-1 w-16 rounded-md border px-2 py-1 text-sm" 169 + /> 170 + </label> 171 + </div> 172 + <input 173 + bind:value={inviteNote} 174 + placeholder="Note (optional)" 175 + class="border-base-300 dark:border-base-700 dark:bg-base-900 w-full rounded-md border px-2 py-1.5 text-sm" 176 + /> 177 + <button 178 + type="submit" 179 + disabled={inviteBusy} 180 + class="bg-base-900 dark:bg-base-50 dark:text-base-900 rounded-md px-3 py-1.5 text-sm font-medium text-white disabled:opacity-50" 181 + >Create invite</button 182 + > 183 + </form> 184 + 185 + {#if lastInviteToken} 186 + <div 187 + class="mb-6 rounded-md border border-amber-300 bg-amber-50 p-3 text-sm dark:border-amber-800 dark:bg-amber-950/40" 188 + > 189 + <div class="mb-1 font-medium">Invite URL — copy now, not shown again:</div> 190 + <code class="dark:bg-base-900 block rounded bg-white p-2 font-mono text-xs break-all" 191 + >{inviteUrl(lastInviteToken)}</code 192 + > 193 + </div> 194 + {/if} 195 + 196 + <ul class="divide-base-200 dark:divide-base-800 divide-y"> 197 + {#each data.invites as inv (inv.tokenHash)} 198 + <li class="flex items-start justify-between py-2 text-sm"> 199 + <div> 200 + <div class="font-medium">{inv.note || '(no note)'}</div> 201 + <div class="text-base-500 text-xs"> 202 + uses {inv.usedCount}{inv.maxUses ? `/${inv.maxUses}` : ''} 203 + {#if inv.expiresAt}• expires {new Date(inv.expiresAt).toLocaleDateString()}{/if} 204 + {#if inv.revokedAt}• <span class="text-red-600">revoked</span>{/if} 205 + </div> 206 + </div> 207 + {#if !inv.revokedAt} 208 + <button class="text-xs text-red-600" onclick={() => handleRevoke(inv.tokenHash)} 209 + >Revoke</button 210 + > 211 + {/if} 212 + </li> 213 + {/each} 214 + </ul> 215 + {/if} 216 + </div>
+1 -1
src/routes/(app)/p/atmosphereconf.org/+page.server.ts
··· 19 19 limit: 200 20 20 }), 21 21 locals.did 22 - ? client.get('community.lexicon.calendar.rsvp.listRecords', { 22 + ? client.get('rsvp.atmo.rsvp.listRecords', { 23 23 params: { actor: locals.did, limit: 200 } 24 24 }) 25 25 : null
+1 -1
vite.config.ts
··· 9 9 server: { 10 10 host: '127.0.0.1', 11 11 port: DEV_PORT, 12 - allowedHosts: ['scroll-heat-flowers-software.trycloudflare.com'] 12 + allowedHosts: ['kid-retention-reservation-proc.trycloudflare.com'] 13 13 } 14 14 });