A Pen by Tibor Martini on CodePen.
Created
October 29, 2023 13:38
-
-
Save tibor/a7c2dd96dc2474a9df62ab24493455d9 to your computer and use it in GitHub Desktop.
eYxNmZK
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id="app"> | |
<div v-cloak> | |
<ol v-if="!access.access"> | |
<li> | |
<p>We need your login data to get your Follows (i.e. accounts that you follow). Please use an App Password.</p> | |
<form> | |
<div class="df fww"> | |
<label class="ml05 mt05 fs0625 ttu db"> | |
Handle | |
<input type="text" v-model="login.identifier" class="pt05 pr05 pb05 pl05 fs100 db" placeholder="@account.bsky.social"> | |
</label> | |
<label class="ml05 mt05 fs0625 ttu"> | |
Password | |
<input type="text" v-model="login.password" class="pt05 pr05 pb05 pl05 fs100 db" placeholder="abcd-abcd-abcd-abcd"> | |
</label> | |
</div> | |
<input type="submit" value="Login" @click.prevent="newSession()" class="pt05 pr05 pb05 pl05 border br bs fs0625 ttu bgcgreen cgreen ml05 mt10"> | |
</form> | |
<div v-if="login.isAppPwd !== null && !login.isAppPwd" class="bgcyellow cyellow border br pt025 pr05 pb025 pl05 mt05">Please use an <a href="https://bsky.app/settings/app-passwords" target="_blank" class="fw800">app password</a> instead of your regular password</div> | |
</li> | |
<li class="mt05">We get all follows of your follows (i.e. accounts in your network)</li> | |
<li class="mt05">We show you a list of accounts sorted by popularity in your network (i.e. accounts which are followed by the most people in your network)</li> | |
</ol> | |
<div v-else class="df"> | |
<button @click.prevent="logout()" class="pt05 pr05 pb05 pl05 br bs ttu fs0625 border">Logout</button> | |
</div> | |
<div v-if="loadingMyFollowings" class="mt10"> | |
<div>Loading your Follows</div> | |
</div> | |
<div v-else-if="progress.value > 0" class="mt05"> | |
<div class="mt10">You follow {{progress.max}} Accounts at the moment</div> | |
<label class="db fs0625"> | |
{{progress.value}} of {{progress.max}} Accounts checked | |
<progress :value="progress.value" :max="progress.max" class="db"></progress> | |
</label> | |
</div> | |
<table class="lsn mt20"> | |
<tr v-for="acc in accountArray" :key="acc.did"> | |
<td class="mt05"> | |
<img :src="acc.avatar" width=40 height=40 loading=lazy class="br100"> | |
</td> | |
<td class="mt05 ml05"> | |
<div class="df jcsb fww"> | |
<div class="fw800 fs100" :title="acc.description">{{acc.displayName}} | |
</div> | |
<div class="fs0625 ttu"> | |
<span v-if="acc.labels.find(label => label?.val === 'impersonation')" class="bgcred cred border br pr025 pl025">🚨Impersonation</span> | |
<span v-if="acc?.viewer?.followedBy" class="bgcyellow cyellow border br pr025 pl025">😊 Follows you</span> | |
</div> | |
</div> | |
<div class="fs0625"> <a :href="`https://bsky.app/profile/${acc.handle}`" target="_blank" class="noa">@{{acc.handle}}</a></div> | |
</td> | |
<td class="mt05 ml05"> | |
<button @click.prevent="followUnfollow(acc)" :class="`pt025 pr05 pb025 pl05 br fs0625 ttu fw800 bn bs border cp ${acc.viewer.following ? 'bgcred cred' : ''}`">{{acc.viewer.following ? 'Unfollow' : 'Follow'}}</button> | |
</td> | |
</tr> | |
</table> | |
</div> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { createApp } = Vue; | |
createApp({ | |
data() { | |
return { | |
login: { | |
identifier: "", | |
password: "", | |
isAppPwd: null | |
}, | |
access: {}, | |
usersImFollowing: {}, | |
accountList: {}, | |
accountArray: [], | |
loadingMyFollowings: false, | |
progress: { | |
value: 0, | |
max: 0 | |
} | |
}; | |
}, | |
methods: { | |
checkPwd() { | |
let regex = /([a-z\d]{4}\-){4}/; | |
return (this.login.isAppPwd = regex.test( | |
this.login.password + "-" | |
)); | |
}, | |
logout() { | |
this.access.length = 0; | |
localStorage.clear(); | |
}, | |
requestTokens(url, options) { | |
return fetch(url, options) | |
.then((req) => req.json()) | |
.then((data) => { | |
this.access = { | |
access: data.accessJwt, | |
refresh: data.refreshJwt, | |
did: data.did | |
}; | |
Object.entries(this.access).forEach(([key, value]) => { | |
localStorage.setItem(key, value); | |
}); | |
}); | |
}, | |
newSession() { | |
if (!this.checkPwd()) return; | |
if (this.access.access) return; | |
let headers = new Headers(); | |
headers.append("Content-Type", "application/json"); | |
let loginData = JSON.stringify({ | |
identifier: this.login.identifier, | |
password: this.login.password | |
}); | |
let options = { | |
headers, | |
body: loginData, | |
method: "POST" | |
}; | |
this.requestTokens( | |
"https://bsky.social/xrpc/com.atproto.server.createSession", | |
options | |
).then(() => this.getMyFollowings(data.did)); | |
}, | |
getUsersFollowings(did, cursor) { | |
let headers = new Headers(); | |
headers.append("Content-Type", "application/json"); | |
headers.append("Authorization", "Bearer " + this.access.access); | |
options = { | |
headers | |
}; | |
return fetch( | |
`https://bsky.social/xrpc/app.bsky.graph.getFollows?actor=${did}&limit=100${ | |
cursor ? "&cursor=" + cursor : "" | |
}`, | |
options | |
) | |
.then((resp) => { | |
if (!resp.ok) throw Error(resp.status_text); | |
return resp.json(); | |
}) | |
.catch((err) => { | |
console.log("Error getUsersFollowing", err); | |
//timeout and handle again | |
}); | |
}, | |
getMyFollowings(handle, cursor) { | |
this.loadingMyFollowings = true; | |
this.getUsersFollowings(handle, cursor) | |
.then((data) => { | |
data.follows.forEach((follow) => { | |
this.usersImFollowing[follow["did"]] = { | |
...follow | |
}; | |
this.progress.max++; | |
}); | |
if (data?.cursor) { | |
this.pause(0).then(() => | |
this.getMyFollowings(handle, data.cursor) | |
); | |
} else { | |
this.loadingMyFollowings = false; | |
Promise.all( | |
Object.keys(this.usersImFollowing).map((user) => | |
this.getMyFollowingsFollowings(user) | |
) | |
).then((values) => console.log(values)); | |
} | |
}) | |
.catch((err) => { | |
console.log("Error getMyFollowings", err); | |
}); | |
}, | |
getMyFollowingsFollowings(did, cursor) { | |
if (!did) { | |
did = Object.keys(this.usersImFollowing)[0]; | |
if (this.usersImFollowing.length === 0) return; | |
} | |
this.getUsersFollowings(did, cursor) | |
.then((data) => { | |
data.follows.forEach((user) => { | |
//if (!!user?.viewer?.following) return; | |
if (user.did === this.access.did) return; | |
this.accountList[user["did"]] = { | |
...user, | |
followers: | |
(this.accountList[user["did"]]?.followers || | |
0) + 1 | |
}; | |
}); | |
if (data?.cursor) { | |
this.getMyFollowingsFollowings(did, data.cursor); | |
} else { | |
this.progress.value++; | |
delete this.usersImFollowing[did]; | |
//this.getMyFollowingsFollowings(); | |
this.pause(0).then(() => this.sortAccounts()); | |
} | |
}) | |
.catch((err) => | |
console.log("Error getFollowingsFollowings", err, did) | |
); | |
}, | |
followUnfollow(acc) { | |
console.log(`Funktion followUnfollow`, acc); | |
let handle = acc.handle; | |
let did = acc.did; | |
let follow = !!acc?.viewer?.following; | |
let url = "https://bsky.social/xrpc/com.atproto.repo.createRecord"; | |
let payload = { | |
repo: this.access.did, | |
collection: "app.bsky.graph.follow", | |
record: { | |
subject: did, | |
createdAt: "2030-10-18T05:31:12.156888Z" | |
} | |
}; | |
if (follow) { | |
delete payload.record; | |
payload["rkey"] = acc?.viewer?.following | |
?.split("/") | |
?.splice(-1)[0]; | |
url = "https://bsky.social/xrpc/com.atproto.repo.deleteRecord"; | |
} | |
let headers = new Headers(); | |
headers.append("Content-Type", "application/json"); | |
headers.append("Authorization", `Bearer ${this.access.access}`); | |
let options = { | |
headers, | |
body: JSON.stringify(payload), | |
method: "POST" | |
}; | |
fetch(url, options) | |
.then((resp) => { | |
if (resp.ok && resp.json()) { | |
return resp.json(); | |
} else if (resp.ok) { | |
return; | |
} else { | |
return Promise.reject(resp); | |
} | |
}) | |
.then((data) => { | |
this.accountList[did].viewer.following = data?.uri; | |
console.log( | |
`Function followUnfollow fertig ${handle}`, | |
data | |
); | |
}) | |
.catch((err) => { | |
console.log( | |
`Error followUnfollow`, | |
err.status, | |
err.statusText | |
); | |
//err.json().then((a) => console.log(a)); | |
}); | |
}, | |
sortAccounts() { | |
this.accountArray = Object.values(this.accountList).sort( | |
(a, b) => b.followers - a.followers | |
); | |
this.accountArray = this.accountArray.slice(0, 250); | |
}, | |
pause(ms) { | |
return new Promise((resolve) => setTimeout(resolve, ms)); | |
} | |
}, | |
mounted() { | |
if (!localStorage.getItem("refresh")) return; | |
let headers = new Headers(); | |
headers.append("Content-Type", "application/json"); | |
headers.append( | |
"Authorization", | |
"Bearer " + localStorage.getItem("refresh") | |
); | |
let options = { | |
headers, | |
method: "POST" | |
}; | |
this.requestTokens( | |
"https://bsky.social/xrpc/com.atproto.server.refreshSession", | |
options | |
).then(() => this.getMyFollowings(this.access.did)); | |
} | |
}).mount("#app"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.2.37/vue.global.prod.min.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
:root { | |
font-size: 100%; | |
margin: 0 auto; | |
padding: 0; | |
--lh: 1; | |
--white: #fff; | |
font-family: sans-serif; | |
max-width: 320px; | |
} | |
[v-cloak] { | |
display: none; | |
} | |
.df { | |
display: flex; | |
} | |
.db { | |
display: block; | |
} | |
.fww { | |
flex-wrap: wrap; | |
} | |
.jcsb { | |
justify-content: space-between; | |
} | |
.fs100 { | |
font-size: 1rem; | |
} | |
.fs0625 { | |
font-size: calc(0.625rem * var(--lh)); | |
line-height: calc(var(--lh) / 0.625); | |
} | |
.fw800 { | |
font-weight: 800; | |
} | |
.ttu { | |
text-transform: uppercase; | |
letter-spacing: 0.1px; | |
} | |
.border { | |
border: 1px solid currentColor; | |
} | |
.pt05 { | |
padding-top: calc(0.5rem * var(--lh)); | |
} | |
.pt025 { | |
padding-top: calc(0.25rem * var(--lh)); | |
} | |
.pr05 { | |
padding-right: calc(0.5rem * var(--lh)); | |
} | |
.pr025 { | |
padding-right: calc(0.25rem * var(--lh)); | |
} | |
.pb05 { | |
padding-bottom: calc(0.5rem * var(--lh)); | |
} | |
.pb025 { | |
padding-bottom: calc(0.25rem * var(--lh)); | |
} | |
.pl05 { | |
padding-left: calc(0.5rem * var(--lh)); | |
} | |
.pl025 { | |
padding-left: calc(0.25rem * var(--lh)); | |
} | |
.mt20 { | |
margin-top: calc(2rem * var(--lh)); | |
} | |
.mt10 { | |
margin-top: calc(1rem * var(--lh)); | |
} | |
.mt05 { | |
margin-top: calc(0.5rem * var(--lh)); | |
} | |
.ml05 { | |
margin-left: calc(0.5rem * var(--lh)); | |
} | |
.lsn { | |
list-style: none; | |
} | |
.bn { | |
border: none; | |
} | |
.noa { | |
text-decoration: none; | |
color: currentColor; | |
} | |
.br { | |
border-radius: 4px; | |
} | |
.br100 { | |
border-radius: 1000px; | |
} | |
.bs { | |
box-shadow: 0rem 0.05rem 0.1rem -0.05rem hsla(0deg, 0%, 0%, 0.5); | |
} | |
.bs:hover { | |
box-shadow: 0rem 0.15rem 0.25rem -0.2rem hsla(0deg, 0%, 0%, 0.5); | |
} | |
.bs:active { | |
box-shadow: 0rem 0.05rem 0.1rem -0.05rem hsla(0deg, 0%, 0%, 0.5) inset; | |
} | |
.bgcred { | |
background-color: hsl(0 75% 80% / 1); | |
} | |
.bgcyellow { | |
background-color: hsl(60 75% 80% / 1); | |
} | |
.bgcgreen { | |
background-color: hsl(120 75% 80% / 1); | |
} | |
.cfff { | |
color: var(--white); | |
} | |
.cred { | |
color: hsl(0 80% 20% / 1); | |
} | |
.cyellow { | |
color: hsl(40 50% 20% / 1); | |
} | |
.cp { | |
cursor: pointer; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment