diff --git a/.gitignore b/.gitignore
index c237551..7bcef76 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.idea
-./config
\ No newline at end of file
+config
+main*
\ No newline at end of file
diff --git a/README.md b/README.md
index 1e1ba62..99650d3 100644
--- a/README.md
+++ b/README.md
@@ -27,36 +27,11 @@ Plugins of aw-sync-suite Agent are used to extend the functionality of the agent
| Plugin | Description | Has Config | Config File | Documentation |
|-----------|-----------------------------------|------------|--------------------------|---------------------------------------------------------------------|
| `filters` | Filters the data of ActivityWatch | ✅ | `aw-plugin-filters.yaml` | [📄](https://github.com/phrp720/aw-sync-suite-plugins/wiki/Filters) |
+| `scripts` | Runs third party Scripts | ✅ | `aw-plugin-scripts.yaml` | [📄](https://github.com/phrp720/aw-sync-suite-plugins/wiki/Scripts) |
## ⚙️ How It Works
-When the `aw-sync-agent` starts, it initializes the chosen plugins specified in the configuration. The workflow is as follows:
-
-1. **Initialization**:
- - Upon startup, the agent reads the `aw-sync-settings.yaml` file to identify which plugins are enabled.
- - Each plugin is initialized according to its specific requirements, preparing it for data processing.
-
-2. **Data Synchronization**:
- - At every sync cycle, the agent collects data from the local ActivityWatch instance.
- - The collected data is then passed through the initialized plugins sequentially.
-
-3. **Data Processing**:
- - Each plugin processes the data according to its defined functionality (e.g., filtering, transformation).
- - After processing, the plugins return the final dataset that has been modified or filtered as per the plugin logic.
-
-4. **Data Push to Prometheus**:
- - The final processed data is then securely pushed to the Prometheus database using the remote-write feature.
-
-This modular approach allows for flexible and customizable data processing, ensuring that only the desired data is sent to Prometheus while maintaining the integrity and confidentiality of sensitive information.
-
-#### flow-diagram:
-
-
-
- 
-
-
-
+For documentation about the plugin workflow in `aw-sync-agent`, please refer [here](https://github.com/phrp720/aw-sync-suite-plugins/wiki/%E2%9A%99%EF%B8%8F-Plugin-Workflow).
## 🛠️ How to create a plugin
diff --git a/config-examples/aw-plugin-filters.yaml b/config-examples/aw-plugin-filters.yaml
new file mode 100644
index 0000000..7d2c785
--- /dev/null
+++ b/config-examples/aw-plugin-filters.yaml
@@ -0,0 +1,205 @@
+Filters:
+ - Filter:
+ filter-name: Email Category
+ enable: true
+ watchers: []
+ target:
+ - key: app
+ value: Google.*
+ - key: title
+ value: Gmail|Thunderbird|mutt|alpine|Yahoo|Hotmail
+ category: Email
+
+ - Filter:
+ filter-name: Programming Category
+ enable: true
+ watchers: []
+ target:
+ - key: app
+ value: Google.*
+ - key: title
+ value: GitHub|Stack Overflow|BitBucket|Gitlab|vim|Spyderbeans|eclipse|visual studio|visual studio code|emacs|vim|nano|sublime|notepad
+ enable: true
+ watchers: []
+ target:
+ - key: app
+ value: Google.*
+ - key: title
+ value: GIMP|Inkscape|Photoshop|Krita|Paint|Paint3D|Paint.NET|PaintShop|CorelDRAW|Illustrator|Lightroom|Acorn|Affinity|Pixelmator|Sketch|Figma|Gravit|Vectr|Canva|Photopea|SumoPaint|Pinta|Seashore|MyPaint|TuxPaint|KolourPaint|Karbon|CinePaint
+ category: Image
+
+ - Filter:
+ filter-name: 3D Category
+ enable: true
+ watchers: []
+ target:
+ - key: app
+ value: Google.*
+ - key: title
+ value: Blender|Maya|3ds Max|Cinema 4D|ZBrush|Houdini|Modo|LightWave|Rhino|SketchUp|AutoCAD|SolidWorks|Fusion 360|Tinkercad|OpenSCAD|FreeCAD|Sculptris|SculptGL|Clara.io|Vectary|Onshape|Shapr3D|SelfCAD|BlocksCAD|Morphi|TinkerCAD|Tinkercad|TinkerCAD
+ category: "3D"
+
+ - Filter:
+ filter-name: Audio Category
+ enable: true
+ watchers: []
+ target:
+ - key: app
+ value: Google.*
+ - key: title
+ value: Audacity|Ardour|LMMS|Reaper|FL Studio|Ableton Live|Pro Tools|GarageBand|Logic Pro|Cubase|Reason|Bitwig Studio|Studio One|Cakewalk|Waveform|Tracktion|Mixcraft|Audition|Sound Forge|Acid Pro|FL Studio|Ableton Live|Pro Tools|GarageBand|Logic Pro|Cubase|Reason|Bitwig Studio|Studio One|Cakewalk|Waveform|Tracktion|Mixcraft|Audition|Sound Forge|Acid Pro|FL Studio|Ableton Live|Pro Tools|GarageBand|Logic Pro|Cubase|Reason|Bitwig Studio|Studio One|Cakewalk|Waveform|Tracktion|Mixcraft|Audition|Sound Forge|Acid Pro|FL Studio|Ableton Live|Pro Tools|GarageBand|Logic Pro|Cubase|Reason|Bitwig Studio|Studio One|Cakewalk|Waveform|Tracktion|Mixcraft|Audition|Sound Forge|Acid Pro|FL Studio|Ableton Live|Pro Tools|GarageBand|Logic Pro|Cubase|Reason|Bitwig Studio|Studio One|Cakewalk|Waveform|Tracktion|Mixcraft|Audition|Sound Forge|Acid Pro|FL Studio|Ableton Live|Pro Tools|GarageBand|Logic Pro|Cubase|Reason|Bitwig Studio|Studio One|Cakewalk|Waveform|Tracktion|Mixcraft|Audition|Sound Forge|Acid Pro|FL Studio|Ableton Live|Pro Tools|GarageBand|Logic Pro|Cubase|Reason|Bitwig Studio|Studio One|Cakewalk|Waveform|Tracktion|Mixcraft|Audition|Sound Forge|Acid Pro|FL Studio|Ableton Live|Pro Tools|GarageBand|Logic Pro|Cubase|Reason|Bitwig Studio|Studio One|Cakewalk|Waveform|Tracktion|Mixcraft|Audition|Sound Forge|Acid Pro|FL Studio|Ableton Live|Pro Tools|GarageBand|Logic Pro|Cubase|Reason|Bitwig Studio|Studio One|Cakewalk|Waveform|Tracktion|Mixcraft|Audition|Sound
+ category: Audio
+
+ - Filter:
+ filter-name: Video Edit Category
+ enable: true
+ watchers: []
+ target:
+ - key: app
+ value: Google.*
+ - key: title
+ value: Kdenlive|Shotcut|OpenShot|DaVinci Resolve|Lightworks|HitFilm Express|Avidemux|Blender
+ category: Video Edit
+
+ - Filter:
+ filter-name: Video Category
+ enable: true
+ watchers: []
+ target:
+ - key: app
+ value: Google.*
+ - key: title
+ value: YouTube|Plex|VLC
+ category: Video
+
+ - Filter:
+ filter-name: Social Media Category
+ enable: true
+ watchers: []
+ target:
+ - key: app
+ value: Google.* | Reddit | Facebook | Twitter | Instagram | devRant | LinkedIn | Pinterest | Snapchat | Tumblr | TikTok
+ - key: title
+ value: reddit|Facebook|Twitter|Instagram|devRant|LinkedIn|Pinterest|Snapchat|Tumblr|TikTok
+ category: Social Media
+
+ - Filter:
+ filter-name: Music Category
+ enable: true
+ watchers: []
+ target:
+ - key: app
+ value: Google.* | Spotify | Deezer | Apple Music | Amazon Music | Tidal | Pandora | SoundCloud | YouTube Music
+ - key: title
+ value: Spotify|Deezer|Apple Music|Amazon Music|Tidal|Pandora|SoundCloud|YouTube Music
+ category: Email
+
+ - Filter:
+ filter-name: Communication Category
+ enable: true
+ watchers: []
+ target:
+ - key: app
+ value: Google.* | Slack | Discord | Zoom | Microsoft Teams | Jitsi Meet | Cisco Webex | BlueJeans | GoToMeeting | Zoho Meeting
+ - key: title
+ value: Messenger|Telegram|Signal|WhatsApp|Rambox|Slack|Riot|Element|Discord|Nheko|NeoChat|Mattermost|Zulip|Rocket.Chat|Jitsi|Jami|Wire|Tox|Threema|Viber|Skype|Zoom|Microsoft Teams|Jitsi Meet|Cisco Webex|BlueJeans|GoToMeeting|Zoho Meeting
+ category: Communication
+
+
+
+
+ - Filter:
+ filter-name: "Youtube Plain Filtering" ## Name of the filter (optional)
+ enable: true
+ watchers: ## watchers where the filter will be applied (optional)
+ - "aw-watcher-window"
+
+ target: ## Data Records that if match , do the filtering (mandatory)
+
+ - key: "app" ## key to filter on
+ value: "Google.*" ## value to filter on REGEX
+
+ - key: "title" ## key to filter on
+ value: "YouTube" ## value to filter on REGEX
+
+ plain-replace: ## key value pairs to replace e.g. on the key `title` replace its value with `Email`
+
+ - key: "title" ## key of record
+ value: "YouTube" ## value to replace
+
+ - Filter:
+ filter-name: "Slack Plain Filtering" ## Name of the filter (optional)
+ enable: true
+ watchers: ## watchers where the filter will be applied (optional)
+ - "aw-watcher-window"
+
+ target: ## Data Records that if match , do the filtering (mandatory)
+
+ - key: "app" ## key to filter on
+ value: "Slack.*" ## value to filter on REGEX
+
+ - key: "title" ## key to filter on
+ value: "Slack" ## value to filter on REGEX
+
+ plain-replace: ## key value pairs to replace e.g. on the key `title` replace its value with `Email`
+
+ - key: "title" ## key of record
+ value: "Slack" ## value to replace
+
+
+ - Filter:
+ filter-name: "Linkedin Plain Filtering" ## Name of the filter (optional)
+ enable: true
+ watchers: ## watchers where the filter will be applied (optional)
+ - "aw-watcher-window"
+
+ target: ## Data Records that if match , do the filtering (mandatory)
+
+ - key: "app" ## key to filter on
+ value: "Google.*" ## value to filter on REGEX
+
+ - key: "title" ## key to filter on
+ value: "LinkedIn" ## value to filter on REGEX
+
+ plain-replace: ## key value pairs to replace e.g. on the key `title` replace its value with `Email`
+
+ - key: "title" ## key of record
+ value: "LinkedIn" ## value to replace
+
+ - Filter:
+ filter-name: "VsCode Plain Filtering" ## Name of the filter (optional)
+ enable: true
+ watchers: ## watchers where the filter will be applied (optional)
+ - "aw-watcher-window"
+
+ target: ## Data Records that if match , do the filtering (mandatory)
+
+ - key: "app" ## key to filter on
+ value: "Google.*" ## value to filter on REGEX
+
+ - key: "title" ## key to filter on
+ value: "Visual Studio Code" ## value to filter on REGEX
+
+ plain-replace: ## key value pairs to replace e.g. on the key `title` replace its value with `Email`
+
+ - key: "title" ## key of record
+ value: "VsCode" ## value to replace
+
+ - Filter:
+ filter-name: "Grafana Plain Filtering" ## Name of the filter (optional)
+ enable: true
+ watchers: ## watchers where the filter will be applied (optional)
+ - "aw-watcher-window"
+
+ target: ## Data Records that if match , do the filtering (mandatory)
+
+ - key: "app" ## key to filter on
+ value: "Google.*" ## value to filter on REGEX
+
+ - key: "title" ## key to filter on
+ value: "Grafana" ## value to filter on REGEX
+
+ plain-replace: ## key value pairs to replace e.g. on the key `title` replace its value with `Email`
+
+ - key: "title" ## key of record
+ value: "Grafana" ## value to replace
diff --git a/config-examples/aw-plugin-scripts.yaml b/config-examples/aw-plugin-scripts.yaml
new file mode 100644
index 0000000..d857a13
--- /dev/null
+++ b/config-examples/aw-plugin-scripts.yaml
@@ -0,0 +1,8 @@
+Scripts:
+ - Script:
+ name: "test"
+ path: "/home/tester/Desktop/scripts/test-script.sh"
+ - Script:
+ name: "dummy"
+ path: "/home/tester/Desktop/scripts/dummy-script.sh"
+ timeout: 10
diff --git a/models/constants.go b/models/constants.go
new file mode 100644
index 0000000..7ba95b6
--- /dev/null
+++ b/models/constants.go
@@ -0,0 +1,7 @@
+package models
+
+// Constants for the plugins RAW NAMES
+const (
+ FILTER = "filters"
+ SCRIPT = "scripts"
+)
diff --git a/models/plugin.go b/models/plugin-core.go
similarity index 100%
rename from models/plugin.go
rename to models/plugin-core.go
diff --git a/models/script.go b/models/script.go
new file mode 100644
index 0000000..684a986
--- /dev/null
+++ b/models/script.go
@@ -0,0 +1,11 @@
+package models
+
+type ScriptConfig struct {
+ Scripts []Script `yaml:"Scripts"`
+}
+
+type Script struct {
+ Name string `yaml:"name"` // ScriptName is the name of the script
+ Path string `yaml:"path"` // Path is the path of the script
+ Timeout int `yaml:"timeout"` // Timeout is the timeout of the script in seconds
+}
diff --git a/plugins/filter/operations.go b/plugins/filter/operations.go
index 7ca9f2a..f00b117 100644
--- a/plugins/filter/operations.go
+++ b/plugins/filter/operations.go
@@ -3,6 +3,7 @@ package filter
import (
"fmt"
"github.com/phrp720/aw-sync-agent-plugins/models"
+ "github.com/phrp720/aw-sync-agent-plugins/util"
"log"
"strconv"
"strings"
@@ -43,7 +44,7 @@ func ValidateFilters(filters []models.Filter) ([]models.Filter, int, int, int) {
func GetCategories(filters []models.Filter) []string {
var categories []string
for _, filter := range filters {
- if filter.Category != "" && !contains(categories, filter.Category) {
+ if filter.Category != "" && !util.Contains(categories, filter.Category) {
categories = append(categories, filter.Category)
}
}
@@ -115,7 +116,7 @@ func Replace(data map[string]interface{}, plain []models.PlainReplace, regex []m
func GetMatchingFilters(filters []models.Filter, watcher string) []models.Filter {
var matchingFilters []models.Filter
for _, filter := range filters {
- if len(filter.Watchers) == 0 || contains(filter.Watchers, watcher) {
+ if len(filter.Watchers) == 0 || util.Contains(filter.Watchers, watcher) {
matchingFilters = append(matchingFilters, filter)
}
}
@@ -214,13 +215,3 @@ func PrintCategories(categories []string) {
}
fmt.Println(border)
}
-
-// contains checks if a slice contains a given string
-func contains(slice []string, item string) bool {
- for _, s := range slice {
- if s == item {
- return true
- }
- }
- return false
-}
diff --git a/plugins/filter/plugin.go b/plugins/filter/plugin.go
index dfada5b..80e06af 100644
--- a/plugins/filter/plugin.go
+++ b/plugins/filter/plugin.go
@@ -30,6 +30,11 @@ func (f *Plugin) Initialize() {
}
func (f *Plugin) Execute(events models.Events, watcher string, userID string, includeHostName bool) models.Events {
+
+ if config.Filters == nil {
+ return events
+ }
+
// Implementation
//Apply the filters
var modifiedEvents models.Events
@@ -74,5 +79,5 @@ func (f *Plugin) Name() string {
}
func (f *Plugin) RawName() string {
- return "filters"
+ return models.FILTER
}
diff --git a/plugins/manager.go b/plugins/manager.go
index e6c6fc3..e3b8377 100644
--- a/plugins/manager.go
+++ b/plugins/manager.go
@@ -3,11 +3,15 @@ package plugins
import (
"github.com/phrp720/aw-sync-agent-plugins/models"
"github.com/phrp720/aw-sync-agent-plugins/plugins/filter"
+ "github.com/phrp720/aw-sync-agent-plugins/plugins/script"
)
func Initialize() []models.Plugin {
var plugins []models.Plugin
+
+ // Add all the plugins here
plugins = append(plugins, &filter.Plugin{})
+ plugins = append(plugins, &script.Plugin{})
return plugins
}
diff --git a/plugins/script/config.go b/plugins/script/config.go
new file mode 100644
index 0000000..06edebf
--- /dev/null
+++ b/plugins/script/config.go
@@ -0,0 +1,44 @@
+package script
+
+import (
+ "github.com/phrp720/aw-sync-agent-plugins/models"
+ "gopkg.in/yaml.v3"
+ "io"
+ "log"
+ "os"
+ "path/filepath"
+)
+
+// LoadYAMLConfig Load the YAML config file
+func LoadYAMLConfig(filename string) models.ScriptConfig {
+ file, err := os.Open(filename)
+
+ if err != nil {
+ log.Printf("No %s file found.", filename)
+ } else {
+ log.Printf("Loading scripts from %s file.", filename)
+ defer file.Close()
+ decoder := yaml.NewDecoder(file)
+ if err = decoder.Decode(&config); err != nil && err != io.EOF {
+ log.Fatalf("Error: Failed to decode filters file: %v", err)
+ }
+
+ }
+
+ return config
+}
+
+// CreateConfigFile creates a config file to a given path based on the settings
+func CreateConfigFile(path string, name string) error {
+
+ fullPath := path + name
+ content, err := yaml.Marshal(&config)
+ if err != nil {
+ return err
+ }
+ dir := filepath.Dir(fullPath)
+ if err = os.MkdirAll(dir, 0755); err != nil {
+ return err
+ }
+ return os.WriteFile(fullPath, content, 0644)
+}
diff --git a/plugins/script/operations.go b/plugins/script/operations.go
new file mode 100644
index 0000000..e30be5f
--- /dev/null
+++ b/plugins/script/operations.go
@@ -0,0 +1,50 @@
+package script
+
+import (
+ "fmt"
+ "github.com/phrp720/aw-sync-agent-plugins/models"
+ "github.com/phrp720/aw-sync-agent-plugins/util"
+ "log"
+ "strconv"
+ "strings"
+)
+
+// PrintScripts prints the array of the registered scripts with borders
+func PrintScripts(scripts []string) {
+ log.Print("Scripts Registered:")
+
+ scriptsMap := map[string]string{
+ "Scripts found": strconv.Itoa(len(scripts)),
+ "Scripts": strings.Join(scripts, ", "),
+ }
+
+ maxKeyLength := 0
+ maxValueLength := 0
+ for key, value := range scriptsMap {
+ if len(key) > maxKeyLength {
+ maxKeyLength = len(key)
+ }
+ if len(value) > maxValueLength {
+ maxValueLength = len(value)
+ }
+ }
+
+ borderLength := maxKeyLength + maxValueLength + 7
+ border := strings.Repeat("-", borderLength)
+ fmt.Println(border)
+ for key, value := range scriptsMap {
+ fmt.Printf("| %-*s | %-*s |\n", maxKeyLength, key, maxValueLength, value)
+ }
+ fmt.Println(border)
+}
+
+// GetScriptNames returns the names of the registered scripts
+func GetScriptNames(scripts []models.Script) []string {
+ var scriptNames []string
+ for _, script := range scripts {
+ if script.Name != "" && !util.Contains(scriptNames, script.Name) {
+ scriptNames = append(scriptNames, script.Name)
+ }
+ }
+ return scriptNames
+}
diff --git a/plugins/script/plugin.go b/plugins/script/plugin.go
new file mode 100644
index 0000000..af286a7
--- /dev/null
+++ b/plugins/script/plugin.go
@@ -0,0 +1,107 @@
+package script
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "github.com/phrp720/aw-sync-agent-plugins/models"
+ "github.com/phrp720/aw-sync-agent-plugins/util"
+ "log"
+ "os/exec"
+ "time"
+)
+
+var config models.ScriptConfig
+
+type Plugin struct{}
+
+func (f *Plugin) Initialize() {
+ config = LoadYAMLConfig("./config/" + f.Name())
+ if config.Scripts != nil {
+ PrintScripts(GetScriptNames(config.Scripts))
+ }
+}
+
+func (f *Plugin) Execute(events models.Events, watcher string, userID string, includeHostName bool) models.Events {
+
+ if config.Scripts == nil {
+ return events
+ } else {
+ log.Printf("Scripts")
+ }
+ // Convert events to JSON
+ eventsToJSON, err := json.Marshal(events)
+ if err != nil {
+ log.Printf("Error marshalling events: %v", err)
+ return events
+ }
+
+ for _, script := range config.Scripts {
+
+ if !util.FileExists(script.Path) {
+ log.Printf("Error running %s : %s", script.Name, "No such file or directory.")
+ continue
+ } else {
+ log.Printf("Running %s", script.Name)
+
+ }
+
+ // Set a default timeout of 30 seconds
+ if script.Timeout == 0 {
+ script.Timeout = 30
+ }
+
+ // Create a context with a timeout
+ ctx, cancel := context.WithTimeout(context.Background(), time.Duration(script.Timeout)*time.Second)
+ defer cancel()
+
+ cmd := exec.CommandContext(ctx, script.Path)
+ // Set up stdin and stdout
+ cmd.Stdin = bytes.NewReader(eventsToJSON)
+ var stdout bytes.Buffer
+ cmd.Stdout = &stdout
+
+ // Run the command
+ err = cmd.Run()
+ if errors.Is(ctx.Err(), context.DeadlineExceeded) {
+ log.Printf("Error running %s : %s", script.Name, "Timeout exceeded")
+ return events
+
+ }
+ if err != nil {
+ log.Printf("Error running %s : %v", script.Name, err)
+ return events
+ }
+ // Read the output and convert it back to events
+ var EventsBuffer models.Events
+ err = json.Unmarshal(stdout.Bytes(), &EventsBuffer)
+ if err != nil {
+ log.Printf("Error unmarshalling script output: %v", err)
+ return events
+ }
+ if EventsBuffer != nil {
+ events = EventsBuffer
+ log.Printf("Finished %s ", script.Name)
+
+ }
+
+ }
+
+ return events
+}
+
+func (f *Plugin) ReplicateConfig(path string) {
+ err := CreateConfigFile(path, f.Name())
+ if err != nil {
+ log.Print(err)
+ }
+}
+
+func (f *Plugin) Name() string {
+ return "aw-plugin-" + f.RawName() + ".yaml"
+}
+
+func (f *Plugin) RawName() string {
+ return models.SCRIPT
+}
diff --git a/tests/script_test.go b/tests/script_test.go
new file mode 100644
index 0000000..937801e
--- /dev/null
+++ b/tests/script_test.go
@@ -0,0 +1,6 @@
+package tests
+
+import "testing"
+
+func TestScript(t *testing.T) {
+}
diff --git a/util/util.go b/util/util.go
new file mode 100644
index 0000000..747a690
--- /dev/null
+++ b/util/util.go
@@ -0,0 +1,21 @@
+package util
+
+import "os"
+
+// Contains checks if a slice contains a given string
+func Contains(slice []string, item string) bool {
+ for _, s := range slice {
+ if s == item {
+ return true
+ }
+ }
+ return false
+}
+
+func FileExists(filename string) bool {
+ info, err := os.Stat(filename)
+ if os.IsNotExist(err) {
+ return false
+ }
+ return !info.IsDir()
+}