This commit is contained in:
2026-01-30 11:49:04 +03:00
commit f8bb05a652
48 changed files with 9538 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
package main
import (
plug "git.wilix.dev/loop/loop-plugin-starter-template/server/plugin"
"github.com/mattermost/mattermost/server/public/plugin"
)
func main() {
p := &plug.Plugin{}
p.InitApi()
plugin.ClientMain(p)
}
+68
View File
@@ -0,0 +1,68 @@
package plugin
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost/server/public/plugin"
"net/http"
"path/filepath"
)
type httpResponse struct {
Status string `json:"status"`
Error string `json:"error,omitempty"`
Data interface{} `json:"data,omitempty"`
}
func (p *Plugin) sendError(w http.ResponseWriter, err string) {
errEnc := json.NewEncoder(w).Encode(httpResponse{
Status: "error",
Error: err,
})
if errEnc != nil {
p.API.LogError("cant encode error response", "error", errEnc)
}
}
func (p *Plugin) sendRes(w http.ResponseWriter, resp httpResponse) {
errEnc := json.NewEncoder(w).Encode(resp)
if errEnc != nil {
p.API.LogError("cant encode error response", "error", errEnc)
}
}
func (p *Plugin) InitApi() {
p.router = mux.NewRouter()
p.router.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if userID := r.Header.Get("Mattermost-User-Id"); userID != "" {
next.ServeHTTP(w, r)
return
}
http.Error(w, "Not authorized", http.StatusUnauthorized)
})
})
// Add your API routes here
p.router.HandleFunc("/example", p.handleExample).Methods("GET")
p.router.HandleFunc("/assets/{fileName}", p.handleAssetFile).Methods("GET")
}
func (p *Plugin) ServeHTTP(_ *plugin.Context, w http.ResponseWriter, r *http.Request) {
p.router.ServeHTTP(w, r)
}
func (p *Plugin) handleAssetFile(w http.ResponseWriter, r *http.Request) {
fileName := mux.Vars(r)["fileName"]
http.ServeFile(w, r, filepath.Join(p.bundlePath, "assets", fileName))
}
func (p *Plugin) handleExample(w http.ResponseWriter, r *http.Request) {
p.sendRes(w, httpResponse{
Status: "OK",
Data: map[string]interface{}{
"message": "Hello from template plugin",
},
})
}
+75
View File
@@ -0,0 +1,75 @@
package plugin
import (
"reflect"
"github.com/pkg/errors"
)
type Configuration struct {
// Add your configuration fields here
}
// Clone shallow copies the Configuration. Your implementation may require a deep copy if
// your Configuration has reference types.
func (c *Configuration) Clone() *Configuration {
var clone = *c
return &clone
}
// GetConfiguration retrieves the active Configuration under lock, making it safe to use
// concurrently. The active Configuration may change underneath the client of this method, but
// the struct returned by this API call is considered immutable.
func (p *Plugin) GetConfiguration() *Configuration {
p.configurationLock.RLock()
defer p.configurationLock.RUnlock()
if p.configuration == nil {
return &Configuration{}
}
return p.configuration
}
// SetConfiguration replaces the active Configuration under lock.
//
// Do not call SetConfiguration while holding the configurationLock, as sync.Mutex is not
// reentrant. In particular, avoid using the plugin API entirely, as this may in turn trigger a
// hook back into the plugin. If that hook attempts to acquire this lock, a deadlock may occur.
//
// This method panics if SetConfiguration is called with the existing Configuration. This almost
// certainly means that the Configuration was modified without being cloned and may result in
// an unsafe access.
func (p *Plugin) SetConfiguration(configuration *Configuration) {
p.configurationLock.Lock()
defer p.configurationLock.Unlock()
p.API.LogInfo("Setting configuration")
if configuration != nil && p.configuration == configuration {
// Ignore assignment if the Configuration struct is empty. Go will optimize the
// allocation for same to point at the same memory address, breaking the check
// above.
if reflect.ValueOf(*configuration).NumField() == 0 {
return
}
p.API.LogInfo("Panic in SetConfiguration")
panic("SetConfiguration called with the existing Configuration")
}
p.configuration = configuration
}
// OnConfigurationChange is invoked when Configuration changes may have been made.
func (p *Plugin) OnConfigurationChange() error {
p.API.LogInfo("OnConfigurationChange")
var configuration = new(Configuration)
// Load the public Configuration fields from the Mattermost server Configuration.
err := p.API.LoadPluginConfiguration(configuration)
if err == nil {
p.SetConfiguration(configuration)
return nil
} else {
return errors.Wrap(err, "failed to load plugin Configuration")
}
}
+56
View File
@@ -0,0 +1,56 @@
package plugin
import (
"sync"
"git.wilix.dev/loop/loop-plugin-starter-template/server/telemetry"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/pluginapi"
)
var buildHash string
var rudderWriteKey string
var rudderDataplaneURL string
// Plugin implements the interface expected by the Mattermost server to communicate between the server and plugin processes.
type Plugin struct {
plugin.MattermostPlugin
IsReady bool
bundlePath string
configurationLock sync.RWMutex
configuration *Configuration
sdk *pluginapi.Client
router *mux.Router
mut sync.RWMutex
telemetry *telemetry.Client
}
func (p *Plugin) OnActivate() error {
p.API.LogInfo("Activating template plugin...")
p.sdk = pluginapi.NewClient(p.API, p.Driver)
if p.router == nil {
p.InitApi()
}
configuration := p.GetConfiguration()
p.configuration = configuration
bundlePath, err := p.API.GetBundlePath()
if err != nil {
return err
}
p.bundlePath = bundlePath
p.IsReady = true
return nil
}
func (p *Plugin) OnDeactivate() error {
p.API.LogInfo("Deactivating template plugin...")
if err := p.uninitTelemetry(); err != nil {
p.API.LogError(err.Error())
}
return nil
}
+3
View File
@@ -0,0 +1,3 @@
package plugin
// Add your store utility functions here
+47
View File
@@ -0,0 +1,47 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"git.wilix.dev/loop/loop-plugin-starter-template/server/telemetry"
)
func (p *Plugin) uninitTelemetry() error {
p.mut.Lock()
defer p.mut.Unlock()
if p.telemetry == nil {
return nil
}
return p.telemetry.Close()
}
func (p *Plugin) initTelemetry(enableDiagnostics *bool) error {
p.mut.Lock()
defer p.mut.Unlock()
if p.telemetry == nil && enableDiagnostics != nil && *enableDiagnostics {
p.API.LogDebug("Initializing telemetry")
// setup telemetry
client, err := telemetry.NewClient(telemetry.ClientConfig{
WriteKey: rudderWriteKey,
DataplaneURL: rudderDataplaneURL,
DiagnosticID: p.API.GetDiagnosticId(),
DefaultProps: map[string]any{
"ServerVersion": p.API.GetServerVersion(),
"PluginBuild": buildHash,
},
})
if err != nil {
return err
}
p.telemetry = client
} else if p.telemetry != nil && (enableDiagnostics == nil || !*enableDiagnostics) {
p.API.LogDebug("Deinitializing telemetry")
// destroy telemetry
if err := p.telemetry.Close(); err != nil {
return err
}
p.telemetry = nil
}
return nil
}
+76
View File
@@ -0,0 +1,76 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package telemetry
import (
"fmt"
"github.com/rudderlabs/analytics-go"
)
type ClientConfig struct {
WriteKey string
DataplaneURL string
DiagnosticID string
DefaultProps map[string]any
}
func (c *ClientConfig) isValid() error {
if c.WriteKey == "" {
return fmt.Errorf("WriteKey should not be empty")
}
if c.DataplaneURL == "" {
return fmt.Errorf("DataplaneURL should not be empty")
}
if c.DiagnosticID == "" {
return fmt.Errorf("DiagnosticID should not be empty")
}
return nil
}
type Client struct {
config ClientConfig
client analytics.Client
}
func NewClient(config ClientConfig) (*Client, error) {
if err := config.isValid(); err != nil {
return nil, fmt.Errorf("telemetry: config validation failed: %w", err)
}
return &Client{
config: config,
client: analytics.New(config.WriteKey, config.DataplaneURL),
}, nil
}
func (c *Client) Track(event string, props map[string]any, ctx *analytics.Context) error {
if props == nil {
props = map[string]any{}
}
for k, v := range c.config.DefaultProps {
props[k] = v
}
if err := c.client.Enqueue(analytics.Track{
Event: event,
UserId: c.config.DiagnosticID,
Properties: props,
Context: ctx,
}); err != nil {
return fmt.Errorf("telemetry: failed to track event: %w", err)
}
return nil
}
func (c *Client) Close() error {
if err := c.client.Close(); err != nil {
return fmt.Errorf("telemetry: failed to close client: %w", err)
}
return nil
}
+31
View File
@@ -0,0 +1,31 @@
package utils
import (
"bytes"
"encoding/json"
"io"
"net/http"
)
func MakeRequest(method, url string, body interface{}) (*http.Response, error) {
var reqBody io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, err
}
reqBody = bytes.NewBuffer(jsonBody)
}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
return nil, err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
client := &http.Client{}
return client.Do(req)
}