Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Scripts Plugin #1

Merged
merged 8 commits into from
Feb 6, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea
./config
config
main*
29 changes: 2 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:

<div align="center">

![flow](plugins-flow-diagram.png)

</div>

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

205 changes: 205 additions & 0 deletions config-examples/aw-plugin-filters.yaml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions config-examples/aw-plugin-scripts.yaml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions models/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package models

// Constants for the plugins RAW NAMES
const (
FILTER = "filters"
SCRIPT = "scripts"
)
File renamed without changes.
11 changes: 11 additions & 0 deletions models/script.go
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 3 additions & 12 deletions plugins/filter/operations.go
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 6 additions & 1 deletion plugins/filter/plugin.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions plugins/manager.go
Original file line number Diff line number Diff line change
@@ -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
}
44 changes: 44 additions & 0 deletions plugins/script/config.go
Original file line number Diff line number Diff line change
@@ -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)
}
50 changes: 50 additions & 0 deletions plugins/script/operations.go
Original file line number Diff line number Diff line change
@@ -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
}
107 changes: 107 additions & 0 deletions plugins/script/plugin.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions tests/script_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package tests

import "testing"

func TestScript(t *testing.T) {
}
21 changes: 21 additions & 0 deletions util/util.go
Original file line number Diff line number Diff line change
@@ -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()
}