...

Source file src/codeberg.org/tslocum/sriracha/internal/server/plugin.go

Documentation: codeberg.org/tslocum/sriracha/internal/server

     1  package server
     2  
     3  import (
     4  	"flag"
     5  	"fmt"
     6  	"html/template"
     7  	"io/fs"
     8  	"log"
     9  	"mime/multipart"
    10  	"net/http"
    11  	"os"
    12  	"path/filepath"
    13  	"plugin"
    14  	"reflect"
    15  	"strings"
    16  
    17  	"codeberg.org/tslocum/sriracha"
    18  	. "codeberg.org/tslocum/sriracha/model"
    19  )
    20  
    21  type rulesHandler func(db sriracha.DB, board *Board) (template.HTML, error)
    22  
    23  type rulesHandlerInfo struct {
    24  	Name    string
    25  	Handler rulesHandler
    26  }
    27  
    28  type attachHandler func(db sriracha.DB, post *Post, file multipart.File) (handled bool, err error)
    29  
    30  type attachHandlerInfo struct {
    31  	Name    string
    32  	Handler attachHandler
    33  }
    34  
    35  type embedHandler func(db sriracha.DB, post *Post, embedURL string) (handled bool, err error)
    36  
    37  type embedHandlerInfo struct {
    38  	Name    string
    39  	Handler embedHandler
    40  }
    41  
    42  type postHandler func(db sriracha.DB, post *Post) error
    43  
    44  type postHandlerInfo struct {
    45  	Name    string
    46  	Handler postHandler
    47  }
    48  
    49  type insertHandler func(db sriracha.DB, post *Post) error
    50  
    51  type insertHandlerInfo struct {
    52  	Name    string
    53  	Handler insertHandler
    54  }
    55  
    56  type createHandler func(db sriracha.DB, post *Post) error
    57  
    58  type createHandlerInfo struct {
    59  	Name    string
    60  	Handler createHandler
    61  }
    62  
    63  type reportHandler func(db sriracha.DB, post *Post) error
    64  
    65  type reportHandlerInfo struct {
    66  	Name    string
    67  	Handler reportHandler
    68  }
    69  
    70  type auditHandler func(db sriracha.DB, user string, action string, info string) error
    71  
    72  type auditHandlerInfo struct {
    73  	Name    string
    74  	Handler auditHandler
    75  }
    76  
    77  type serveHandler func(db sriracha.DB, a *Account, w http.ResponseWriter, r *http.Request) (template.HTML, error)
    78  
    79  type serveHandlerInfo struct {
    80  	Name    string
    81  	Handler serveHandler
    82  }
    83  
    84  type pluginInfo struct {
    85  	ID       int
    86  	Name     string
    87  	FullName string
    88  	About    string
    89  	Help     template.HTML
    90  	Config   []sriracha.PluginConfig
    91  	Events   []string
    92  	Serve    serveHandler
    93  }
    94  
    95  var (
    96  	allPlugins              []any
    97  	allPluginInfo           []*pluginInfo
    98  	allPluginRulesHandlers  []rulesHandlerInfo
    99  	allPluginAttachHandlers []attachHandlerInfo
   100  	allPluginEmbedHandlers  []embedHandlerInfo
   101  	allPluginPostHandlers   []postHandlerInfo
   102  	allPluginInsertHandlers []insertHandlerInfo
   103  	allPluginCreateHandlers []createHandlerInfo
   104  	allPluginReportHandlers []reportHandlerInfo
   105  	allPluginAuditHandlers  []auditHandlerInfo
   106  	allPluginServeHandlers  []serveHandlerInfo
   107  )
   108  
   109  // registerPlugin registers a Sriracha plugin to start receiving events.
   110  func (s *Server) registerPlugin(plugin any) {
   111  	info := &pluginInfo{
   112  		ID: len(allPlugins) + 1,
   113  	}
   114  
   115  	v := reflect.ValueOf(plugin)
   116  	if v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {
   117  		v = v.Elem()
   118  	}
   119  	info.FullName = v.Type().Name()
   120  	info.Name = strings.ToLower(info.FullName)
   121  
   122  	if pAbout, ok := plugin.(sriracha.Plugin); ok {
   123  		info.About = pAbout.About()
   124  	} else {
   125  		log.Fatalf("%s does not implement required methods", info.Name)
   126  	}
   127  
   128  	if pHelp, ok := plugin.(sriracha.PluginWithHelp); ok {
   129  		info.Help = pHelp.Help()
   130  	}
   131  
   132  	if pConfig, ok := plugin.(sriracha.PluginWithConfig); ok {
   133  		config := pConfig.Config()
   134  		for i := range config {
   135  			err := config[i].Validate()
   136  			if err != nil {
   137  				optionName := config[i].Name
   138  				if strings.TrimSpace(optionName) == "" {
   139  					optionName = fmt.Sprintf("#%d", i)
   140  				} else {
   141  					optionName = fmt.Sprintf(`"%s"`, optionName)
   142  				}
   143  				log.Fatalf("%s configuration option %s is invalid: %s", info.Name, optionName, err)
   144  			} else if config[i].Type == sriracha.TypeBoolean && config[i].Default == "" {
   145  				config[i].Default = "0"
   146  			}
   147  
   148  			if config[i].Type == sriracha.TypeEnum {
   149  				config[i].Value = ""
   150  			} else {
   151  				config[i].Value = config[i].Default
   152  			}
   153  		}
   154  		info.Config = config
   155  	}
   156  
   157  	if _, ok := plugin.(sriracha.PluginWithUpdate); ok {
   158  		info.Events = append(info.Events, "Update")
   159  	}
   160  
   161  	if pRules, ok := plugin.(sriracha.PluginWithRules); ok {
   162  		info.Events = append(info.Events, "Rules")
   163  		allPluginRulesHandlers = append(allPluginRulesHandlers, rulesHandlerInfo{strings.ToLower(info.Name), pRules.Rules})
   164  	}
   165  
   166  	if pAttach, ok := plugin.(sriracha.PluginWithAttach); ok {
   167  		info.Events = append(info.Events, "Attach")
   168  		allPluginAttachHandlers = append(allPluginAttachHandlers, attachHandlerInfo{strings.ToLower(info.Name), pAttach.Attach})
   169  	}
   170  
   171  	if pEmbed, ok := plugin.(sriracha.PluginWithEmbed); ok {
   172  		info.Events = append(info.Events, "Embed")
   173  		allPluginEmbedHandlers = append(allPluginEmbedHandlers, embedHandlerInfo{strings.ToLower(info.Name), pEmbed.Embed})
   174  	}
   175  
   176  	if pPost, ok := plugin.(sriracha.PluginWithPost); ok {
   177  		info.Events = append(info.Events, "Post")
   178  		allPluginPostHandlers = append(allPluginPostHandlers, postHandlerInfo{strings.ToLower(info.Name), pPost.Post})
   179  	}
   180  
   181  	if pInsert, ok := plugin.(sriracha.PluginWithInsert); ok {
   182  		info.Events = append(info.Events, "Insert")
   183  		allPluginInsertHandlers = append(allPluginInsertHandlers, insertHandlerInfo{strings.ToLower(info.Name), pInsert.Insert})
   184  	}
   185  
   186  	if pCreate, ok := plugin.(sriracha.PluginWithCreate); ok {
   187  		info.Events = append(info.Events, "Create")
   188  		allPluginCreateHandlers = append(allPluginCreateHandlers, createHandlerInfo{strings.ToLower(info.Name), pCreate.Create})
   189  	}
   190  
   191  	if pReport, ok := plugin.(sriracha.PluginWithReport); ok {
   192  		info.Events = append(info.Events, "Report")
   193  		allPluginReportHandlers = append(allPluginReportHandlers, reportHandlerInfo{strings.ToLower(info.Name), pReport.Report})
   194  	}
   195  
   196  	if pAudit, ok := plugin.(sriracha.PluginWithAudit); ok {
   197  		info.Events = append(info.Events, "Audit")
   198  		allPluginAuditHandlers = append(allPluginAuditHandlers, auditHandlerInfo{strings.ToLower(info.Name), pAudit.Audit})
   199  	}
   200  
   201  	if pServe, ok := plugin.(sriracha.PluginWithServe); ok {
   202  		info.Events = append(info.Events, "Serve")
   203  		info.Serve = pServe.Serve
   204  		allPluginServeHandlers = append(allPluginServeHandlers, serveHandlerInfo{strings.ToLower(info.Name), pServe.Serve})
   205  	}
   206  
   207  	if len(info.Events) == 0 {
   208  		info.Events = append(info.Events, "None")
   209  	}
   210  
   211  	allPlugins = append(allPlugins, plugin)
   212  	allPluginInfo = append(allPluginInfo, info)
   213  }
   214  
   215  func (s *Server) loadPlugin(pluginPath string) error {
   216  	wrapErr := func(err error) error {
   217  		return fmt.Errorf("failed to load plugin %s: %s", pluginPath, err)
   218  	}
   219  
   220  	info, err := os.Stat(pluginPath)
   221  	if err != nil {
   222  		return wrapErr(err)
   223  	} else if info.IsDir() {
   224  		return filepath.WalkDir(pluginPath, func(path string, d fs.DirEntry, err error) error {
   225  			if err != nil {
   226  				return err
   227  			} else if d.IsDir() || path == pluginPath {
   228  				return nil
   229  			}
   230  			return s.loadPlugin(path)
   231  		})
   232  	} else if !strings.HasSuffix(pluginPath, ".so") {
   233  		return nil
   234  	}
   235  
   236  	const pluginExample = "plugins must declare a function named \"Plugin\" which returns a new instance:\n  func Plugin() any {\n    return &MyPlugin{}\n  }"
   237  	plugin, err := plugin.Open(pluginPath)
   238  	if err != nil {
   239  		return wrapErr(err)
   240  	}
   241  	pluginSymbol, err := plugin.Lookup("Plugin")
   242  	if err != nil {
   243  		return wrapErr(fmt.Errorf("expected function \"Plugin\" was not found: " + pluginExample))
   244  	}
   245  	pluginFunc, ok := pluginSymbol.(func() any)
   246  	if !ok {
   247  		return wrapErr(fmt.Errorf("symbol \"Plugin\" was found but does not match the expected function signature: " + pluginExample))
   248  	}
   249  	s.registerPlugin(pluginFunc())
   250  	return nil
   251  }
   252  
   253  func (s *Server) loadPlugins() error {
   254  	for _, pluginPath := range flag.Args() {
   255  		err := s.loadPlugin(pluginPath)
   256  		if err != nil {
   257  			return err
   258  		}
   259  	}
   260  	if len(allPluginInfo) != 0 {
   261  		var plural string
   262  		if len(allPluginInfo) != 1 {
   263  			plural = "s"
   264  		}
   265  		var names []string
   266  		for _, info := range allPluginInfo {
   267  			names = append(names, info.FullName)
   268  		}
   269  		fmt.Printf("Loaded %d plugin%s: %s.\n", len(allPluginInfo), plural, strings.Join(names, ", "))
   270  	}
   271  	return nil
   272  }
   273  

View as plain text