Files
Ken Johnson e378651919 Add complete hello world example plugin with web admin page
Demonstrates: lifecycle, routes, admin page, database, events, config,
scheduled tasks, inter-plugin calls, and proper library exports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:25:15 -07:00

412 lines
13 KiB
ObjectPascal

library hello;
{ =========================================================================
Hello World Plugin for Fastway BBS
A minimal example plugin that demonstrates:
- Plugin lifecycle (Create, Initialize, Finalize, Destroy)
- REST API route registration
- Admin web page registration
- Database table creation
- Configuration defaults
- Event handling
- Scheduled tasks
- Logging through the host interface
Build:
cd examples/hello
make
Install:
cp libhello.so /opt/fastway/plugins/
mkdir -p /opt/fastway/plugins/hello/web
cp web/hello.html web/hello.js /opt/fastway/plugins/hello/web/
Restart the server.
========================================================================= }
{$mode objfpc}{$H+}
{$INTERFACES CORBA}
uses
{$IFDEF UNIX}cmem, cthreads,{$ENDIF} { cmem MUST be first — shared heap }
Classes, SysUtils, fpjson,
fw_plugin_api;
const
HELLO_VERSION = '1.0.0';
type
TFWHelloPlugin = class(TInterfacedObject, IFWPlugin, IFWRoutePlugin,
IFWAdminPlugin, IFWDatabasePlugin, IFWEventPlugin, IFWConfigPlugin,
IFWScheduledPlugin)
private
FHost: IFWPluginHost;
FGreetCount: Integer;
{ Route handlers }
procedure HandleGreet(const AMethod, APath: string; ARequestBody: TJSONObject;
out AResponseCode: Integer; out AResponseBody: TJSONObject);
procedure HandleStats(const AMethod, APath: string; ARequestBody: TJSONObject;
out AResponseCode: Integer; out AResponseBody: TJSONObject);
public
{ IFWPlugin — metadata }
function GetName: string;
function GetVersion: string;
function GetDescription: string;
function GetAuthor: string;
function GetDependencies: TStringArray;
function GetPermissions: TFWPermissionSet;
function GetCapabilities: TFWPluginCapabilities;
function GetTarget: TFWPluginTarget;
{ IFWPlugin — lifecycle }
function Initialize(AHost: IFWPluginHost): Boolean;
procedure Finalize;
{ IFWPlugin — interface casting }
function AsProtocol: IFWProtocolPlugin;
function AsRoute: IFWRoutePlugin;
function AsAdmin: IFWAdminPlugin;
function AsDBPlugin: IFWDatabasePlugin;
function AsEvent: IFWEventPlugin;
function AsConfig: IFWConfigPlugin;
function AsService: IFWServicePlugin;
function AsProtocolDetector: IFWProtocolDetector;
function AsScheduled: IFWScheduledPlugin;
{ IFWRoutePlugin — REST API }
procedure RegisterRoutes;
procedure UnregisterRoutes;
{ IFWAdminPlugin — web admin }
procedure RegisterAdminPages;
function GetAdminMenuItems: TJSONArray;
{ IFWDatabasePlugin — schema }
function GetSchemaVersion: Integer;
procedure CreateSchema;
procedure MigrateSchema(AFromVersion: Integer);
{ IFWEventPlugin — event handling }
procedure RegisterEvents;
function GetSubscribedEvents: TStringArray;
procedure HandleEvent(const AEventType: string; AData: TJSONObject);
{ IFWConfigPlugin — configuration }
function GetConfigDefaults: TJSONObject;
procedure OnConfigChanged(const AKey, AValue: string);
{ IFWScheduledPlugin — periodic tasks }
function GetScheduledTasks: TJSONArray;
procedure RunTask(const ATaskName: string);
{ IFWPlugin — inter-plugin calls }
function GetExportedFunctions: TStringArray;
function CallFunction(const AName: string; AArgs: TJSONObject): TJSONObject;
end;
{ ======================================================================== }
{ Plugin metadata }
{ ======================================================================== }
function TFWHelloPlugin.GetName: string;
begin Result := 'hello'; end;
function TFWHelloPlugin.GetVersion: string;
begin Result := HELLO_VERSION; end;
function TFWHelloPlugin.GetDescription: string;
begin Result := 'Hello World — example plugin for Fastway BBS'; end;
function TFWHelloPlugin.GetAuthor: string;
begin Result := 'Fastway BBS'; end;
function TFWHelloPlugin.GetDependencies: TStringArray;
begin SetLength(Result, 0); end; { No dependencies }
function TFWHelloPlugin.GetPermissions: TFWPermissionSet;
begin Result := [fpDatabaseRead, fpDatabaseWrite, fpDatabaseCreateTables,
fpConfigRead, fpConfigWrite, fpRoutesRegister, fpAdminMenu,
fpEventsSubscribe, fpEventsFire]; end;
function TFWHelloPlugin.GetCapabilities: TFWPluginCapabilities;
begin Result := [pcRoutes, pcAdmin, pcDatabase, pcEvents, pcConfig, pcScheduled]; end;
function TFWHelloPlugin.GetTarget: TFWPluginTarget;
begin Result := ptServer; end; { Server-side plugin }
{ ======================================================================== }
{ Interface casting — return Self for implemented interfaces, nil otherwise }
{ ======================================================================== }
function TFWHelloPlugin.AsProtocol: IFWProtocolPlugin; begin Result := nil; end;
function TFWHelloPlugin.AsRoute: IFWRoutePlugin; begin Result := Self; end;
function TFWHelloPlugin.AsAdmin: IFWAdminPlugin; begin Result := Self; end;
function TFWHelloPlugin.AsDBPlugin: IFWDatabasePlugin; begin Result := Self; end;
function TFWHelloPlugin.AsEvent: IFWEventPlugin; begin Result := Self; end;
function TFWHelloPlugin.AsConfig: IFWConfigPlugin; begin Result := Self; end;
function TFWHelloPlugin.AsService: IFWServicePlugin; begin Result := nil; end;
function TFWHelloPlugin.AsProtocolDetector: IFWProtocolDetector; begin Result := nil; end;
function TFWHelloPlugin.AsScheduled: IFWScheduledPlugin; begin Result := Self; end;
{ ======================================================================== }
{ Lifecycle }
{ ======================================================================== }
function TFWHelloPlugin.Initialize(AHost: IFWPluginHost): Boolean;
begin
FHost := AHost;
FGreetCount := 0;
FHost.LogInfo('Hello plugin initialized!');
Result := True;
end;
procedure TFWHelloPlugin.Finalize;
begin
FHost.LogInfo('Hello plugin shutting down. Total greets: ' + IntToStr(FGreetCount));
end;
{ ======================================================================== }
{ REST API Routes }
{ ======================================================================== }
procedure TFWHelloPlugin.RegisterRoutes;
begin
{ Routes are registered relative to /api/v1/plugins/<name>/ }
FHost.RouteRegister('GET', '/greet', @Self.HandleGreet);
FHost.RouteRegister('GET', '/stats', @Self.HandleStats);
end;
procedure TFWHelloPlugin.UnregisterRoutes;
begin
{ Nothing to clean up }
end;
procedure TFWHelloPlugin.HandleGreet(const AMethod, APath: string;
ARequestBody: TJSONObject; out AResponseCode: Integer;
out AResponseBody: TJSONObject);
var
Greeting: string;
begin
Inc(FGreetCount);
{ Read greeting from plugin config (stored in system_config table) }
Greeting := FHost.ConfigGet('greeting', 'Hello, World!');
AResponseCode := 200;
AResponseBody := TJSONObject.Create;
AResponseBody.Add('success', True);
AResponseBody.Add('message', Greeting);
AResponseBody.Add('greet_count', FGreetCount);
FHost.LogDebug('Greeted! Total: ' + IntToStr(FGreetCount));
end;
procedure TFWHelloPlugin.HandleStats(const AMethod, APath: string;
ARequestBody: TJSONObject; out AResponseCode: Integer;
out AResponseBody: TJSONObject);
var
Rows: TJSONArray;
begin
{ Query our database table }
Rows := FHost.DBQuery('SELECT COUNT(*) AS total FROM p_hello_log');
AResponseCode := 200;
AResponseBody := TJSONObject.Create;
AResponseBody.Add('success', True);
AResponseBody.Add('greets_this_session', FGreetCount);
if (Rows <> nil) and (Rows.Count > 0) then
begin
AResponseBody.Add('greets_all_time', TJSONObject(Rows.Items[0]).Get('total', 0));
Rows.Free;
end;
end;
{ ======================================================================== }
{ Admin web page }
{ ======================================================================== }
procedure TFWHelloPlugin.RegisterAdminPages;
begin
{ Register the web admin page. The URL maps to:
/plugins/hello/web/hello.html -> #/plugins/hello in the SPA }
FHost.AdminAddMenu('Hello', '/plugins/hello/web/hello.html', '', 99, 'System');
end;
function TFWHelloPlugin.GetAdminMenuItems: TJSONArray;
begin
Result := TJSONArray.Create;
end;
{ ======================================================================== }
{ Database schema }
{ ======================================================================== }
function TFWHelloPlugin.GetSchemaVersion: Integer;
begin
Result := 1; { Increment when schema changes }
end;
procedure TFWHelloPlugin.CreateSchema;
begin
{ Tables are prefixed with p_<pluginname>_ to avoid collisions }
FHost.DBCreateTable(
'CREATE TABLE IF NOT EXISTS p_hello_log (' +
' id INTEGER PRIMARY KEY AUTOINCREMENT,' +
' message TEXT,' +
' created_at DATETIME DEFAULT (datetime(''now''))' +
')');
end;
procedure TFWHelloPlugin.MigrateSchema(AFromVersion: Integer);
begin
{ Handle schema upgrades from older versions }
if AFromVersion < 1 then
CreateSchema;
end;
{ ======================================================================== }
{ Events }
{ ======================================================================== }
procedure TFWHelloPlugin.RegisterEvents;
begin
{ Nothing to register — we just subscribe }
end;
function TFWHelloPlugin.GetSubscribedEvents: TStringArray;
begin
{ Subscribe to user login events }
SetLength(Result, 1);
Result[0] := 'user.login';
end;
procedure TFWHelloPlugin.HandleEvent(const AEventType: string;
AData: TJSONObject);
var
Username: string;
Params: TJSONArray;
begin
if AEventType = 'user.login' then
begin
Username := '';
if Assigned(AData) then
Username := AData.Get('username', 'unknown');
FHost.LogInfo('Hello saw user login: ' + Username);
{ Log it to our database table }
Params := TJSONArray.Create;
try
Params.Add('User ' + Username + ' logged in');
FHost.DBExecParams(
'INSERT INTO p_hello_log (message) VALUES (?)', Params);
finally
Params.Free;
end;
end;
end;
{ ======================================================================== }
{ Configuration }
{ ======================================================================== }
function TFWHelloPlugin.GetConfigDefaults: TJSONObject;
begin
{ These keys are stored in the system_config table under this plugin's namespace.
The web admin System Configuration page shows them automatically. }
Result := TJSONObject.Create;
Result.Add('greeting', 'Hello, World!');
Result.Add('log_logins', 'true');
end;
procedure TFWHelloPlugin.OnConfigChanged(const AKey, AValue: string);
begin
FHost.LogInfo('Config changed: ' + AKey + ' = ' + AValue);
end;
{ ======================================================================== }
{ Scheduled tasks }
{ ======================================================================== }
function TFWHelloPlugin.GetScheduledTasks: TJSONArray;
var
Task: TJSONObject;
begin
Result := TJSONArray.Create;
Task := TJSONObject.Create;
Task.Add('name', 'cleanup');
Task.Add('description', 'Clean up old log entries');
Task.Add('interval', 86400); { Once per day (in seconds) }
Result.Add(Task);
end;
procedure TFWHelloPlugin.RunTask(const ATaskName: string);
begin
if ATaskName = 'cleanup' then
begin
FHost.DBExec(
'DELETE FROM p_hello_log WHERE created_at < datetime(''now'', ''-30 days'')');
FHost.LogInfo('Cleaned up old hello log entries');
end;
end;
{ ======================================================================== }
{ Inter-plugin calls }
{ ======================================================================== }
function TFWHelloPlugin.GetExportedFunctions: TStringArray;
begin
SetLength(Result, 1);
Result[0] := 'Greet';
end;
function TFWHelloPlugin.CallFunction(const AName: string;
AArgs: TJSONObject): TJSONObject;
begin
Result := TJSONObject.Create;
if AName = 'Greet' then
begin
Inc(FGreetCount);
Result.Add('success', True);
Result.Add('message', FHost.ConfigGet('greeting', 'Hello!'));
end
else
begin
Result.Add('success', False);
Result.Add('error', 'Unknown function: ' + AName);
end;
end;
{ ======================================================================== }
{ Library exports — REQUIRED by all Fastway plugins }
{ ======================================================================== }
var
PluginInstance: TFWHelloPlugin;
function FWPluginAPIVersion: Integer; cdecl;
begin
Result := FW_PLUGIN_API_VERSION;
end;
function FWPluginCreate(AHost: IFWPluginHost): IFWPlugin; cdecl;
begin
PluginInstance := TFWHelloPlugin.Create;
Result := PluginInstance;
end;
procedure FWPluginDestroy(APlugin: IFWPlugin); cdecl;
begin
if Assigned(PluginInstance) then
FreeAndNil(PluginInstance);
end;
exports
FWPluginAPIVersion,
FWPluginCreate,
FWPluginDestroy;
begin
end.