WriteFreely as a Knowledgebase for Discord
Right click discord messages and save them to a blog.
I run FreedomAcademy with my friends, it's a Discord server that helps people build discipline and overcome addictions.
It's a wholesome place, with everyone helping each other get better. In the process, there's a lot of advice that's sent around, I wanted something blog-like to house useful messages.
Our options were:
- Pinned Messages: Discord-exclusive, with a limit of 50 per channel
- Starboard: Discord-exclusive, community-curated, hard to search
Our current system relied on the starboard, there weren't any faults with it per say, but I wanted to play around!
I looked at a few projects: Ghost, Microfeed, WriteFreely, and others.
Ghost is great software, but it felt like overkill. Microfeed looked like too much effort to setup, so I went with the simplest of them all WriteFreely.
WriteFreely is a light-weight writing platform that works out of the box. It supports multiple blogs (great for discord, we can have one for each user), and ActivityPub - allowing it to mesh well with the Fediverse. The latter was icing on the cake.
WriteFreely API
The idea is to take a Discord message and turn it into a blogpost:
WriteFreely categorizes posts into 'Collections', to us, they correspond to authors. All of your messages will be grouped under a collection bearing your name.
As posts are no longer on Discord, Users might want to hide their posts from the internet. We want to archive knowledge (the same way a pin or a starboard would), and respect their privacy. For this, we want to let authors toggle their collection's visibility between public and private.
So here's what needs to be done:
- Create a collection for the author if it doesn't exist
- Create a new post for the message in the collection
- Add an option for a user to toggle their collection's privacy
Authenticating
First, we want an access token. This is returned after logging in with a username and password,
async def get_writefreely_access_token(username: str, password: str) -> str:
"""Log in to the configured WriteFreely instance and return an access token."""
async with aiohttp.ClientSession() as session:
async with session.post(
f"https://fa.rambhat.la/api/auth/login",
json={
"alias": username,
"pass": password,
},
headers={"content-type": "application/json"},
) as response:
auth_data = await response.json()
WRITEFREELY_ACCESS_TOKEN = auth_data["data"][
"access_token"
] # No error handling. Live by the Sword. Die by the Sword. lol
return WRITEFREELY_ACCESS_TOKEN
The returned token will be sent in future requests in the authorization header:
headers = {"Authorization": f"Token {WRITEFREELY_ACCESS_TOKEN}"}
Creating Collections (Blogs)
Collections are translated to Blogs on the Website, we want to create one for each author. Here's what they look like:
Collections have titles and aliases, the title corresponds to the author's username with an alias from their discord ID.
First, let's try retrieving the collection from the ID through the /collections/{id}
endpoint. If successful, the response will contain a data
key that we're interested in. If the collection doesn't exist, we'll get a non-200 status code, or a non-json content-type. In either of these cases, we'll have to create a new collection first.
A POST Request to /collections
with the following JSON Data will create the collection and return its data.
{"alias": id, "title": title}
Creating a Post
The next step is to create a post to the author's collection. (This is the second step after a message is sent to be archived).
A POST Request to /collections/{id}/posts
with the following data
{
"body": body,
"title": title,
"created": "%Y-%m-%dT%H:%M:%SZ"
}
is all we need!
Toggling Collection Visibility
This isn't triggered during the archival process, but when the user modifies their Bot preferences.
When this happens, we first want to create a collection for them if it doesn't exist, and then change the collection's visibility.
content-type
must be application/x-www-form-urlencoded
The docs specify
application/json
, which fails :/An additional authentication cookie may be required.
These haven't expired in the 8 months I've used them, I'm hoping they don't expire lol
A POST Request to /collections/{id}
with the following data
{
"visibility": ("1" if visible else "2")
}, # 1: Public, 2: Private.
and headers
{
"content-type": "application/x-www-form-urlencoded",
"Cookie": "your_cookie_here" # After logging in on the website, open developer tools, make a request, and copy the value of the `Cookie` header
}
That's it!
Discord App Menu (Message Command)
I wanted to implement something using the Discord App Menu, this was a good opportunity.
Right clicking a message, going to apps, and hitting archive post (faster to do than to type it haha) should trigger our functions.
Discord calls these 'Message Commands', we can use disnake's @commands.message_command
decorator to implement the same.
@commands.message_command(name="Archive Post")
async def archive_post(
self, inter: disnake.MessageCommandInteraction, message: disnake.Message
):
await inter.response.defer(ephemeral=True)
config = read_config()
post_body = message.content
collection = await get_or_create_writefreely_collection(
message.author.id, message.author.name
)
post_id = await create_writefreely_collection_post(
message.author.id,
post_body,
created_at=message.created_at,
)
await message.add_reaction("🗃️")
await inter.send(
embed=disnake.Embed(
title="Created Post", description=f"Post ID: `{post_id}`"
),
ephemeral=True,
)
Though, attached files and images aren't carried over. Our solution at FA was to link to any attached files at the end of the post.
That's it!
Deployment Data
{
"LiveURL": "https://fa.rambhat.la",
"Repository": "https://gist.github.com/TheOnlyWayUp/d136a9afc6de7413040427316c474d6a"
}