A brief money extension example
I figured it would be good to give an explanation of how I see larger-scale extensions being built. An example is probably the best way to explain. Please keep in mind this code is not production code but more just a basis to give you an idea.
The repo for this commentary is here: https://github.com/forumspark/money-extension-example
It is a Laravel Breeze (PHP) app but I will point to the most important things, they will be applicable in whichever language.
This example includes the ability for forum users to gain "money" or "credits" for making topics and posts. They have the option to make a donation to other users. This is as far as the example goes because it demonstrates listening to webhooks, showing some additional data for users and performing (a sort of) authenticated user action.
So on to it. The first thing we need to do is set up the forum in our application. The board owner needs to tell us their Board URL, an API token and some other basic configuration.
From that we'll store their details in our database and tell ForumSpark to start sending us webhooks: https://github.com/forumspark/money-extension-example/blob/master/app/Http/Controllers/ForumController.php#L18
<?php
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $request->api_key,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
])
->post("https://{$request->board_url}.forumspark.net/api/v1/webhooks", [
'topics' => ['topics/create', 'replies/create'],
'url' => route('webhooks')
]);
if (! $response->successful()) {
throw ValidationException::withMessages(['api_key' => $response->body()]);
}
$signingSecret = $response->json('signing_secret');
Forum::create([
'user_id' => auth()->id(),
'board_url' => $request->board_url,
'api_key' => $request->api_key,
'shop_name' => $request->shop_name,
'shop_currency' => $request->shop_currency,
'shop_currency_prefix' => $request->shop_currency_prefix,
'shop_currency_suffix' => $request->shop_currency_suffix,
'signing_secret' => $signingSecret,
]);
Here we make a request to ForumSpark using the provided API key to tell it to start sending webhooks relating to the topics replies/create and topics/create. That is, whenever someone creates a post or topic we want to be notified about it. And we tell ForumSpark to reach us at /webhooks of our application.
Next we need to start listening to those webhooks: https://github.com/forumspark/money-extension-example/blob/master/routes/web.php#L11
$board = $request->header('X-ForumSpark-Board');
$signature = $request->header('X-ForumSpark-Signature');
if (! $forum = Forum::whereBoardUrl(Str::of($board)->remove('.forumspark.net'))->first()) {
// the board is not set up, we'll return a 200 response so FS does not keep
// retrying a board that doesn't exist
return;
}
$generatedHmac = hash_hmac('sha256', $request->getContent(), $forum->signing_secret);
if (! hash_equals($generatedHmac, $signature)) {
\Log::error("Generated HMAC does not match incoming signature.");
abort(401);
}
Firstly we compare the incoming signature with a generated signature on our end. We generate a hmac using the SHA256 algorithm, the request body and the shared secret we got from the first step.
$topic = $request->json('topic');
if (! $user = FSUser::whereFsUserId($request->json('data.user_id'))->first()) {
$user = FSUser::create([
'forum_id' => $forum->id,
'fs_user_id' => $request->json('data.user_id')
]);
}
// pull this from user config
$topicMap = [
'topics/create' => 5,
'replies/create' => 2,
];
$user->update(['credits' => $user->credits + $topicMap[$topic] ?? 0]);
Next we listen to the type of event and then update (or create) our user accordingly. We have to create because it is possible that we receive an event for a user we do not know about.
Now that we have a way for users to accumulate money, we need a way to show it. This is a combination of an api endpoint and a frontend mini profile widget.
The endpoint is simple: https://github.com/forumspark/money-extension-example/blob/master/routes/api.php#L11
There is a middleware that runs before hitting /api/* - this pulls the ForumSpark board URL from the headers and then scopes all data to that specific forum. So when we query /api/users/1 the user data relates to the user from the supplied board.
Otherwise, we look for the user and supply a default value in cases where the user does not exist or the actual data stored in our database (ie. the number of credits they have).
if (! $user = FSUser::whereFsUserId($userId)->first()) {
return [
'credits' => 0,
'exists' => false,
'currency' => $request->forum->shop_currency,
'prefix' => $request->forum->shop_currency_prefix,
'suffix' => $request->forum->shop_currency_suffix,
];
}
return [
'credits' => $user->credits,
'currency' => $request->forum->shop_currency,
'prefix' => $request->forum->shop_currency_prefix,
'suffix' => $request->forum->shop_currency_suffix,
'exists' => true
];
The frontend side of this simply makes a request to this endpoint: https://github.com/forumspark/money-extension-example/blob/master/public/credits.js
It will take a bit of explanation but its worthwhile looking at our docs regarding profile widgets: https://docs.forumspark.net/api/js.html#mini-profile-widgets
Essentially we setup a shared store (so that we dont make repeated requests for users on the same page). When that shared store is initialised, we fetch the user's credit info so that it can be presented in the render function.
fetchCredits() {
fetch(`${url}/users/${user.id}`, {headers: {'X-ForumSpark-Board': 'jamie-test.forumspark.net'}})
.then(res => res.json())
.then(json => {
this.currency = json.currency;
this.prefix = json.prefix;
this.suffix = json.suffix;
this.credits = json.credits
this.loading = false
})
},
and
<div class="credits">
<dl>
<div>
<dt x-text="${store}.currency"></dt>
<dd @click="show = true" x-text="${store}.loading ? 'loading' : ${store}.formattedCredits" />
</div>
</dl>
</div>
respectively.
The donation element takes a similar tactic, we take the frontend_secret (https://docs.forumspark.net/api/js.html#current-user-data) and send it along to a backend endpoint so that it can be checked against the API. If the frontend_secret matches and the user has enough credits then we can make the transaction.
fetch(`${url}/donate`, {
method: "POST",
headers: {
accept: 'application/json',
'content-type': 'application/json',
'X-ForumSpark-Board': 'jamie-test.forumspark.net'
},
body: JSON.stringify({
to_id: user.id,
secret: window.ForumSpark.currentUser.frontend_secret,
user_id: window.ForumSpark.currentUser.id,
amount
})
}).catch(err => console.error(err));
The javascript request
and the relevant backend endpoint: https://github.com/forumspark/money-extension-example/blob/master/routes/api.php#L32
The steps are as follows:
- Check the incoming secret against the ForumSpark API
- Check that the target user exists in the ForumSpark API
- Check that the user donating has enough credits
- Perform the transaction
This is all a bit of a rush (and Im not much of a writer) so I will likely expand on this and improve my explanations but I wanted to put this out there at least as a start for anyone interested.
Of course any questions then please ask away.
Note that I have plans to deprecate the frontend_secret system in favour of short-lived JWTs that can be generated and then verified against the FS API. It will be more secure that way :). But for now this is ok!
Please feel free to take the code, adapt as you need it.
ForumSpark Support

