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
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