Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New app: Bluesky Stats #2881

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions blueskyusers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Bluesky Users

## Overview

This is a simple app that displays the total number of users on the [Bluesky](https://bsky.social/) social network.

Data is gathered from the website https://bsky-users.theo.io.

The app displays an animation of the user count increasing, according to a growth rate returned from the aforementioned website.

---

## Configuration (Schema)

The app supports some configuration options like changing the user count color and using dots instead of commas as thousands separator.

---

## API Details

There is a single and unauthenticated API being used: https://bsky-users.theo.io/api/stats.

We are not sure if there is any kind of rate limiting implemented, but results are cached for 15 minutes using the http module native caching mechanism.

---

## Error Handling

The app has safeguards in place to identify potential errors and always display something on the screen. For instance, a non 200 response from the API will be handled and a message will be shown on the screen.

---

## Future Improvements

There's nothing left to add here since the API only returns the user count and the growth rate.
213 changes: 213 additions & 0 deletions blueskyusers/bluesky_users.star
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
"""
Applet: Bluesky Users
Summary: Display Bluesky user count
Description: Display the total number of users on the Bluesky social network. Data courtesy of https://bsky-users.theo.io.
Author: Daniel Sitnik
"""

load("encoding/base64.star", "base64")
load("http.star", "http")
load("humanize.star", "humanize")
load("math.star", "math")
load("render.star", "render")
load("schema.star", "schema")

DEFAULT_STAT_COLOR = "#3a83f7"
DEFAULT_DOT_SEPARATOR = False
CACHE_TTL = 120
BSKY_LOGO = base64.decode("""
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
""")

def main(config):
"""Main app method.

Args:
config (config): App configuration.

Returns:
render.Root: Root widget tree.
"""

# read config values
number_color = config.str("number_color", DEFAULT_STAT_COLOR)
dot_separator = config.bool("dot_separator", DEFAULT_DOT_SEPARATOR)

# get data
res = http.get("https://bsky-users.theo.io/api/stats", ttl_seconds = CACHE_TTL)

# handle errors
if res.status_code != 200:
print("API error %d: %s" % (res.status_code, res.body()))
return render_api_error(str(int(res.status_code)))

# transform to json
data = res.json()

# read data properties
user_count = data["last_user_count"]
growth_per_second = math.ceil(data["growth_per_second"])

# render frames to represent user count increase
frames = render_frames(user_count, growth_per_second, number_color, dot_separator)

# calculate frame delay to display all frames in 15 seconds
delay = math.ceil(15000 / len(frames))

# render display
return render.Root(
delay = delay,
child = render.Box(
color = "#000000",
child = render.Column(
main_align = "space_evenly",
cross_align = "center",
expanded = True,
children = [
render.Row(
expanded = True,
main_align = "center",
cross_align = "center",
children = [
render.Image(src = BSKY_LOGO, height = 10),
render.Box(width = 2, height = 1, color = "#000000"),
render.Text("Bluesky", font = "Dina_r400-6"),
],
),
render.Animation(
children = frames,
),
render.Text("users", color = "#afbac7", font = "tom-thumb"),
],
),
),
)

def render_frames(user_count, growth_per_second, number_color, dot_separator):
"""Renders the frames for a animation representing the user count increase.

Args:
user_count (int): Current number of users.
growth_per_second (float): User growth rate per second.
number_color (str): Color used to format the user count number.
dot_separator (bool): Indicates if dot should be used as thousands separator.

Returns:
list: List of frames.
"""
frames = []

# calculates how many users we would have after 15 seconds with the current growth rate
last_user_count = int(user_count + growth_per_second * 15)

# diff between final and current count
count_diff = int(last_user_count - user_count)

# create frames
for _ in range(count_diff):
# check if user count has reached another million
if user_count % 1000000 != 0:
# most likely not, so just render the number
frame_text = humanize.comma(int(user_count))
if dot_separator:
frame_text = frame_text.replace(",", ".")
frames.append(render.Text(frame_text, color = number_color))
else:
# reached another million, render frames for a nice flashing animation!
frames += render_million(user_count, number_color, dot_separator)
user_count += 1

return frames

def render_million(user_count, number_color, dot_separator):
"""Renders colored frames to show that the user count has reached another million.

Args:
user_count (int): Current number of users.
number_color (str): Color used to format the user count number.
dot_separator (bool): Indicates if dot should be used as thousands separator.

Returns:
list: List of frames.
"""
frames = []

frame_text = humanize.comma(int(user_count))
if dot_separator:
frame_text = frame_text.replace(",", ".")

# add more frames with rainbow colors
for _ in range(8):
frames.append(render.Text(frame_text, color = number_color))
frames.append(render.Text(frame_text, color = "#ffffff"))
frames.append(render.Text(frame_text, color = "#ff0000"))
frames.append(render.Text(frame_text, color = "#ff7f00"))
frames.append(render.Text(frame_text, color = "#ffff00"))
frames.append(render.Text(frame_text, color = "#00ff00"))
frames.append(render.Text(frame_text, color = "#0000ff"))
frames.append(render.Text(frame_text, color = "#4b0082"))
frames.append(render.Text(frame_text, color = "#9400d3"))
frames.append(render.Text(frame_text, color = number_color))

return frames

def get_schema():
"""Creates the schema for the configuration screen.

Returns:
schema.Schema: The schema for the configuration screen.
"""
return schema.Schema(
version = "1",
fields = [
schema.Color(
id = "number_color",
name = "Number color",
desc = "The color of the user count number.",
icon = "brush",
default = DEFAULT_STAT_COLOR,
palette = [DEFAULT_STAT_COLOR],
),
schema.Toggle(
id = "dot_separator",
name = "Dot separator",
desc = "Use dots as thousands separator.",
icon = "circleDot",
default = DEFAULT_DOT_SEPARATOR,
),
],
)

def render_api_error(status_code):
"""Renders a view when there's an API error.

Args:
status_code (str): The http status code of the error.

Returns:
render.Root: Root widget tree.
"""
return render.Root(
child = render.Box(
color = "#000000",
child = render.Column(
main_align = "space_evenly",
cross_align = "center",
expanded = True,
children = [
render.Row(
expanded = True,
main_align = "center",
cross_align = "center",
children = [
render.Image(src = BSKY_LOGO, height = 10),
render.Box(width = 2, height = 1, color = "#000000"),
render.Text("Bluesky", font = "Dina_r400-6"),
],
),
render.Text("API ERROR", color = "#ff0000"),
render.Text("CODE %d" % status_code, color = "#ffff00"),
],
),
),
)
6 changes: 6 additions & 0 deletions blueskyusers/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
id: bluesky-users
name: Bluesky Users
summary: Display Bluesky user count
desc: Display the total number of users on the Bluesky social network. Data courtesy of https://bsky-users.theo.io.
author: Daniel Sitnik
Loading