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>
412 lines
13 KiB
ObjectPascal
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.
|