Run a giveaway from a bsky post. Choose from those who interacted with it

wip

Changed files
+132 -40
+132 -40
Index.html
··· 37 37 </script> 38 38 39 39 <script> 40 - // links/links/distinct-dids?target=at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r&collection=app.bsky.feed.like&path=.subject.uri 41 - const constellationEndpoint = 'https://constellation.microcosm.blue/links/distinct-dids'; 40 + 41 + const constellationEndpoint = 'https://constellation.microcosm.blue'; 42 + 43 + async function callConstellationEndpoint(target, collection, path, cursor = null) { 44 + try { 45 + const url = new URL(`${constellationEndpoint}/links/distinct-dids`); 46 + url.searchParams.append('target', target); 47 + url.searchParams.append('collection', collection); 48 + url.searchParams.append('path', path); 49 + if (cursor) { 50 + url.searchParams.append('cursor', cursor); 51 + } 52 + const response = await fetch(url); 53 + if (!response.ok) { 54 + throw new Error(`HTTP error! Status: ${response.status}`); 55 + } 56 + 57 + return await response.json(); 58 + } catch (error) { 59 + console.error('Error calling constellation endpoint:', error); 60 + throw error; 61 + } 62 + } 42 63 43 64 function isValidHttpUrl(string) { 44 65 let url; ··· 55 76 //Form input 56 77 post_url: '', 57 78 winner_count: 1, 58 - likes_only: false, 79 + likes_only: true, 59 80 reposts_only: false, 60 81 likes_and_reposts: false, 61 82 62 83 error: '', 84 + loading: false, 85 + winners: [], 86 + showResults: false, 63 87 64 88 validateCheckBoxes(event) { 65 89 const targetId = event.target.id; ··· 69 93 }, 70 94 async runGiveaway() { 71 95 this.error = ''; 72 - //Form validation 73 - if (!isValidHttpUrl(this.post_url)) { 74 - this.error = 'Invalid Bluesky post URL'; 75 - return; 76 - } 77 - 78 - if (this.winner_count < 1) { 79 - this.error = 'SOMEBODY has to win'; 80 - } 81 - 82 - if (!this.likes_only && !this.reposts_only && !this.likes_and_reposts) { 83 - this.error = 'Well, you have to pick some way for them to win'; 84 - return; 85 - } 96 + this.loading = true; 97 + this.winners = []; 98 + this.showResults = false; 86 99 87 - let atUri = ''; 88 - if (this.post_url.startsWith('at://')) { 89 - atUri = this.post_url; 90 - } else { 91 - //More checks to make sure it's a bsky url 92 - if (!this.post_url.startsWith('https://bsky.app/')) { 93 - this.error = 'Link to the Bluesky post or at uri please'; 100 + try { 101 + //Form validation 102 + if (this.winner_count < 1) { 103 + this.error = 'SOMEBODY has to win'; 94 104 return; 95 105 } 96 - const postSplit = this.post_url.split('/'); 97 - if (postSplit.length < 7) { 98 - this.error = 'Invalid Bluesky post URL. Should look like https://bsky.app/profile/baileytownsend.dev/post/3lbq7o74fcc2d'; 106 + 107 + if (!this.likes_only && !this.reposts_only && !this.likes_and_reposts) { 108 + this.error = 'Well, you have to pick some way for them to win'; 99 109 return; 100 110 } 101 - try { 102 - const handle = postSplit[4]; 103 - const recordKey = postSplit[6]; 104 111 105 - let did = await window.resolveHandle(handle); 106 - atUri = `at://${did}/app.bsky.feed.post/${recordKey}`; 112 + let atUri = ''; 113 + if (this.post_url.startsWith('at://')) { 114 + atUri = this.post_url; 115 + } else { 116 + //More checks to make sure it's a bsky url 117 + if (!this.post_url.startsWith('https://bsky.app/')) { 118 + this.error = 'Link to the Bluesky post or at uri please'; 119 + return; 120 + } 121 + const postSplit = this.post_url.split('/'); 122 + if (postSplit.length < 7) { 123 + this.error = 'Invalid Bluesky post URL. Should look like https://bsky.app/profile/baileytownsend.dev/post/3lbq7o74fcc2d'; 124 + return; 125 + } 126 + try { 127 + const handle = postSplit[4]; 128 + const recordKey = postSplit[6]; 107 129 108 - } catch (e) { 109 - console.log(e); 110 - this.error = e.message; 130 + let did = await window.resolveHandle(handle); 131 + atUri = `at://${did}/app.bsky.feed.post/${recordKey}`; 132 + 133 + } catch (e) { 134 + console.log(e); 135 + this.error = e.message; 136 + return; 137 + } 138 + } 139 + 140 + 141 + // Determine which collections to fetch based on user selection 142 + const collections = []; 143 + if (this.likes_only || this.likes_and_reposts) { 144 + collections.push('app.bsky.feed.like'); 145 + } 146 + if (this.reposts_only || this.likes_and_reposts) { 147 + collections.push('app.bsky.feed.repost'); 148 + } 149 + 150 + // Path to extract the subject URI 151 + const path = '.subject.uri'; 152 + 153 + // Fetch data for each collection 154 + const results = []; 155 + let cursor = null; 156 + for (const collection of collections) { 157 + console.log(`Fetching ${collection} data...`); 158 + const response = await callConstellationEndpoint(atUri, collection, path); 159 + console.log(`${collection} response:`, response); 160 + if (response && response.linking_dids) { 161 + cursor = response.cursor; 162 + results.push(...response.linking_dids); 163 + } 164 + } 165 + 166 + // Remove duplicates if fetching both likes and reposts 167 + const uniqueDids = [...new Set(results)]; 168 + console.log('Unique DIDs:', uniqueDids); 169 + 170 + // Select winners 171 + if (uniqueDids.length === 0) { 172 + this.error = 'No participants found for this post'; 111 173 return; 112 174 } 113 - } 114 175 176 + const winnerCount = Math.min(this.winner_count, uniqueDids.length); 115 177 116 - console.log(atUri); 178 + // Randomly select winners 179 + for (let i = 0; i < winnerCount; i++) { 180 + const randomIndex = Math.floor(Math.random() * uniqueDids.length); 181 + this.winners.push(uniqueDids[randomIndex]); 182 + // Remove the winner to avoid duplicates 183 + uniqueDids.splice(randomIndex, 1); 184 + } 117 185 186 + console.log('Winners:', this.winners); 187 + this.showResults = true; 188 + 189 + } catch (error) { 190 + console.error('Error in runGiveaway:', error); 191 + this.error = `Error fetching data: ${error.message}`; 192 + } finally { 193 + this.loading = false; 194 + } 118 195 } 119 196 120 197 })) ··· 178 255 179 256 </fieldset> 180 257 <span x-show="error" x-text="error" class="text-red-500 text-lg font-bold"></span> 181 - <button type="submit" class="btn btn-neutral mt-4">I choose 182 - you! 258 + <button type="submit" class="btn btn-neutral mt-4" x-bind:disabled="loading"> 259 + <span x-show="!loading">I choose you!</span> 260 + <span x-show="loading" class="loading loading-spinner"></span> 183 261 </button> 184 262 </fieldset> 185 263 </form> 186 - <a href="https://tangled.sh/@baileytownsend.dev/at-giveaways" class="link">View on <span 264 + 265 + <!-- Results Section --> 266 + <div x-show="showResults" class="mt-6 p-4 bg-base-200 rounded-lg"> 267 + <h3 class="text-xl font-bold mb-2">🎉 Winners 🎉</h3> 268 + <p class="mb-2">Total participants: <span x-text="winners.length"></span></p> 269 + <ul class="list-disc pl-5"> 270 + <template x-for="winner in winners"> 271 + <li class="mb-1"> 272 + <span x-text="winner"></span> 273 + </li> 274 + </template> 275 + </ul> 276 + </div> 277 + 278 + <a href="https://tangled.sh/@baileytownsend.dev/at-giveaways" class="link mt-4 block">View on <span 187 279 class="font-semibold italic">tangled.sh</span></a> 188 280 </div> 189 281 </div>