diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4def2ba --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,52 @@ +name: Server +on: + - push + +jobs: + build-linux: + name: Linux build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '^1.23' + - name: Get current Go version + run: go version + - name: Get Go dependencies + run: go mod download + - name: Set env + run: go env -w GOFLAGS=-mod=mod + - name: Go get + run: go get -v . + - name: Build app + run: go build -v -o backend main.go + - uses: actions/upload-artifact@v4 + with: + name: backend-linux + path: backend + docker: + name: Docker build + needs: build-linux + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + - name: Get Docker version + run: docker --version + - name: Docker Login + uses: docker/login-action@v3 + with: + username: ${{github.actor}} + password: ${{secrets.GITHUB_TOKEN}} + registry: "ghcr.io" + - name: Downcase repository owner + run: | + echo REPO=$(echo ${{github.repository_owner}} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV + - name: Build Docker image + uses: docker/build-push-action@v3 + with: + context: "." + file: "./Dockerfile" + tags: ghcr.io/${{env.REPO}}/sharepoint-bot:latest + push: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e14b349 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +database.sqlite3 +database +.idea +config.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..974119d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.23-alpine AS builder + +COPY . /app + +WORKDIR /app + +# Add gcc +RUN apk add build-base tzdata + +RUN go mod download && \ + go env -w GOFLAGS=-mod=mod && \ + go get . && \ + go build -v -o backend . + +FROM alpine:latest + +WORKDIR /app +COPY --from=builder /app/backend ./backend + +CMD [ "./backend" ] diff --git a/config-example.json b/config-example.json new file mode 100644 index 0000000..3adf170 --- /dev/null +++ b/config-example.json @@ -0,0 +1 @@ +{"database_name":"sqlite3","database_config":"database/database.sqlite3","debug":true,"ms_oauth2_client_id":"","ms_oauth2_secret":"","ms_oauth2_refresh_token":"","webhooks":["https://discord.com/api/webhooks/channelId/botToken"]} \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..5a24812 --- /dev/null +++ b/config/config.go @@ -0,0 +1,67 @@ +package config + +import ( + "encoding/json" + "os" +) + +type Config struct { + DatabaseName string `json:"database_name"` + DatabaseConfig string `json:"database_config"` + Debug bool `json:"debug"` + MicrosoftOAUTH2ClientID string `json:"ms_oauth2_client_id"` + MicrosoftOAUTH2Secret string `json:"ms_oauth2_secret"` + MicrosoftOAUTH2RefreshToken string `json:"ms_oauth2_refresh_token"` + Webhooks []string `json:"webhooks"` +} + +func GetConfig() (Config, error) { + var config Config + file, err := os.ReadFile("config.json") + if err != nil { + marshal, err := json.Marshal(Config{ + DatabaseName: "sqlite3", + DatabaseConfig: "database.sqlite3", + Debug: true, + MicrosoftOAUTH2ClientID: "", + MicrosoftOAUTH2RefreshToken: "", + MicrosoftOAUTH2Secret: "", + Webhooks: make([]string, 0), + }) + if err != nil { + return config, err + } + err = os.WriteFile("config.json", marshal, 0600) + if err != nil { + return config, err + } + file, err = os.ReadFile("config.json") + if err != nil { + return config, err + } + } + err = json.Unmarshal(file, &config) + if err != nil { + return config, err + } + return config, err +} + +func SaveConfig(config Config) error { + marshal, err := json.Marshal(config) + if err != nil { + return err + } + f, err := os.Create("config.json") + if err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + err = os.WriteFile("config.json", marshal, 0600) + if err != nil { + return err + } + return nil +} diff --git a/db/schema.go b/db/schema.go new file mode 100644 index 0000000..988cd78 --- /dev/null +++ b/db/schema.go @@ -0,0 +1,16 @@ +package db + +const schema string = ` +CREATE TABLE IF NOT EXISTS sharepoint_notifications ( + id VARCHAR(60) PRIMARY KEY, + name VARCHAR, + description VARCHAR, + created_on INTEGER, + modified_on INTEGER, + message_ids JSON, + created_by VARCHAR(100), + modified_by VARCHAR(100), + expires_on INTEGER, + has_attachments BOOLEAN +); +` diff --git a/db/sharepoint_notifications.go b/db/sharepoint_notifications.go new file mode 100644 index 0000000..93dfd52 --- /dev/null +++ b/db/sharepoint_notifications.go @@ -0,0 +1,71 @@ +package db + +type SharepointNotification struct { + ID string `db:"id"` + Name string `db:"name"` + Description string `db:"description"` + CreatedOn int `db:"created_on"` + ModifiedOn int `db:"modified_on"` + CreatedBy string `db:"created_by"` + ModifiedBy string `db:"modified_by"` + MessageIDs string `db:"message_ids"` + ExpiresOn int `db:"expires_on"` + HasAttachments bool `db:"has_attachments"` +} + +func (db *sqlImpl) GetSharepointNotification(id string) (notification SharepointNotification, err error) { + err = db.db.Get(¬ification, "SELECT * FROM sharepoint_notifications WHERE id=$1", id) + return notification, err +} + +func (db *sqlImpl) GetSharepointNotifications() (notification []SharepointNotification, err error) { + err = db.db.Select(¬ification, "SELECT * FROM sharepoint_notifications ORDER BY modified_on ASC") + return notification, err +} + +func (db *sqlImpl) InsertSharepointNotification(notification SharepointNotification) (err error) { + _, err = db.db.NamedExec( + `INSERT INTO sharepoint_notifications + (id, + name, + description, + created_on, + modified_on, + created_by, + modified_by, + message_ids, + expires_on, + has_attachments) +VALUES (:id, + :name, + :description, + :created_on, + :modified_on, + :created_by, + :modified_by, + :message_ids, + :expires_on, + :has_attachments) +`, notification) + return err +} + +func (db *sqlImpl) UpdateSharepointNotification(notification SharepointNotification) error { + _, err := db.db.NamedExec( + `UPDATE sharepoint_notifications SET + name=:name, + description=:description, + modified_on=:modified_on, + modified_by=:modified_by, + message_ids=:message_ids, + expires_on=:expires_on, + has_attachments=:has_attachments +WHERE id=:id`, + notification) + return err +} + +func (db *sqlImpl) DeleteSharepointNotification(id string) error { + _, err := db.db.Exec(`DELETE FROM sharepoint_notifications WHERE id=$1`, id) + return err +} diff --git a/db/sql.go b/db/sql.go new file mode 100644 index 0000000..baffd13 --- /dev/null +++ b/db/sql.go @@ -0,0 +1,35 @@ +package db + +import ( + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" + "go.uber.org/zap" +) + +type sqlImpl struct { + db *sqlx.DB + logger *zap.SugaredLogger +} + +func (db *sqlImpl) Init() { + db.db.MustExec(schema) +} + +type SQL interface { + Init() + + GetSharepointNotification(id string) (notification SharepointNotification, err error) + GetSharepointNotifications() (notification []SharepointNotification, err error) + InsertSharepointNotification(notification SharepointNotification) (err error) + UpdateSharepointNotification(notification SharepointNotification) error + DeleteSharepointNotification(id string) error +} + +func NewSQL(driver string, drivername string, logger *zap.SugaredLogger) (SQL, error) { + db, err := sqlx.Connect(driver, drivername) + return &sqlImpl{ + db: db, + logger: logger, + }, err +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bbd3f39 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.8' +services: + backend: + image: ghcr.io/bezidev/sharepoint-bot + volumes: + - ./config.json:/app/config.json + - ./database:/app/database + environment: + - TZ=Europe/Ljubljana + restart: always + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..68b6baf --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +module SharepointBot + +go 1.23.2 + +require ( + github.com/JohannesKaufmann/html-to-markdown v1.6.0 // indirect + github.com/PuerkitoBio/goquery v1.9.2 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/andybalholm/cascadia v1.3.2 // indirect + github.com/cloudflare/circl v1.4.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/imroc/req/v3 v3.48.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattn/go-sqlite3 v1.14.24 // indirect + github.com/onsi/ginkgo/v2 v2.20.2 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.47.0 // indirect + github.com/refraction-networking/utls v1.6.7 // indirect + go.uber.org/mock v0.4.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/tools v0.25.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3432137 --- /dev/null +++ b/go.sum @@ -0,0 +1,131 @@ +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k= +github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ= +github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= +github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY= +github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= +github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/imroc/req/v3 v3.48.0 h1:IYuMGetuwLzOOTzDCquDqs912WNwpsPK0TBXWPIvoqg= +github.com/imroc/req/v3 v3.48.0/go.mod h1:weam9gmyb00QnOtu6HXSnk44dNFkIUQb5QdMx13FeUU= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y= +github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E= +github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= +github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= +github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..1b98b00 --- /dev/null +++ b/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "SharepointBot/config" + "SharepointBot/db" + "fmt" + "go.uber.org/zap" +) + +func main() { + fmt.Println("Starting server...") + + var logger *zap.Logger + var err error + + cfg, err := config.GetConfig() + if err != nil { + panic("Error while retrieving config: " + err.Error()) + return + } + + if cfg.Debug { + logger, err = zap.NewDevelopment() + } else { + logger, err = zap.NewProduction() + } + if err != nil { + panic(err.Error()) + return + } + + sugared := logger.Sugar() + + database, err := db.NewSQL(cfg.DatabaseName, cfg.DatabaseConfig, sugared) + database.Init() + + if err != nil { + sugared.Fatal("Error while creating database: ", err.Error()) + return + } + + sugared.Info("Database created successfully") + + httphandler := NewHTTPInterface(sugared, database, cfg) + httphandler.SharepointGoroutine() +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..d23ed59 --- /dev/null +++ b/server.go @@ -0,0 +1,26 @@ +package main + +import ( + "SharepointBot/config" + "SharepointBot/db" + "go.uber.org/zap" +) + +type httpImpl struct { + logger *zap.SugaredLogger + db db.SQL + config config.Config +} + +type HTTP interface { + // sharepoint.go + SharepointGoroutine() +} + +func NewHTTPInterface(logger *zap.SugaredLogger, db db.SQL, config config.Config) HTTP { + return &httpImpl{ + logger: logger, + db: db, + config: config, + } +} diff --git a/sharepoint.go b/sharepoint.go new file mode 100644 index 0000000..4aea6f3 --- /dev/null +++ b/sharepoint.go @@ -0,0 +1,527 @@ +package main + +import ( + "SharepointBot/config" + "SharepointBot/db" + "bufio" + "database/sql" + "encoding/json" + "errors" + "fmt" + md "github.com/JohannesKaufmann/html-to-markdown" + "github.com/imroc/req/v3" + "net/http" + "os" + "time" +) + +var SCOPE = "https://graph.microsoft.com/Files.Read.All https://graph.microsoft.com/Sites.Read.All" + +type OAUTH2CallbackBody struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + Code string `json:"code"` + Scope string `json:"scope"` + GrantType string `json:"grant_type"` +} + +type MicrosoftOUATH2Response struct { + TokenType string `json:"token_type"` + Scope string `json:"scope"` + ExpiresIn int `json:"expires_in"` + ExtExpiresIn int `json:"ext_expires_in"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +type SharepointResponse struct { + OdataContext string `json:"@odata.context"` + OdataNextLink string `json:"@odata.nextLink"` + Value []struct { + OdataEtag string `json:"@odata.etag"` + CreatedDateTime time.Time `json:"createdDateTime"` + ETag string `json:"eTag"` + Id string `json:"id"` + LastModifiedDateTime time.Time `json:"lastModifiedDateTime"` + WebUrl string `json:"webUrl"` + CreatedBy struct { + User struct { + Email string `json:"email"` + Id string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"user"` + } `json:"createdBy"` + LastModifiedBy struct { + User struct { + Email string `json:"email"` + Id string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"user"` + } `json:"lastModifiedBy"` + ParentReference struct { + Id string `json:"id"` + SiteId string `json:"siteId"` + } `json:"parentReference"` + ContentType struct { + Id string `json:"id"` + Name string `json:"name"` + } `json:"contentType"` + } `json:"value"` +} + +type SharepointNotificationResponse struct { + OdataContext string `json:"@odata.context"` + OdataEtag string `json:"@odata.etag"` + CreatedDateTime time.Time `json:"createdDateTime"` + ETag string `json:"eTag"` + Id string `json:"id"` + LastModifiedDateTime time.Time `json:"lastModifiedDateTime"` + WebUrl string `json:"webUrl"` + CreatedBy struct { + User struct { + Email string `json:"email"` + Id string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"user"` + } `json:"createdBy"` + LastModifiedBy struct { + User struct { + Email string `json:"email"` + Id string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"user"` + } `json:"lastModifiedBy"` + ParentReference struct { + Id string `json:"id"` + SiteId string `json:"siteId"` + } `json:"parentReference"` + ContentType struct { + Id string `json:"id"` + Name string `json:"name"` + } `json:"contentType"` + FieldsOdataContext string `json:"fields@odata.context"` + Fields struct { + OdataEtag string `json:"@odata.etag"` + Title string `json:"Title"` + ModerationStatus int `json:"_ModerationStatus"` + Body string `json:"Body"` + Expires time.Time `json:"Expires"` + Id string `json:"id"` + ContentType string `json:"ContentType"` + Modified time.Time `json:"Modified"` + Created time.Time `json:"Created"` + AuthorLookupId string `json:"AuthorLookupId"` + EditorLookupId string `json:"EditorLookupId"` + UIVersionString string `json:"_UIVersionString"` + Attachments bool `json:"Attachments"` + Edit string `json:"Edit"` + LinkTitleNoMenu string `json:"LinkTitleNoMenu"` + LinkTitle string `json:"LinkTitle"` + ItemChildCount string `json:"ItemChildCount"` + FolderChildCount string `json:"FolderChildCount"` + ComplianceFlags string `json:"_ComplianceFlags"` + ComplianceTag string `json:"_ComplianceTag"` + ComplianceTagWrittenTime string `json:"_ComplianceTagWrittenTime"` + ComplianceTagUserId string `json:"_ComplianceTagUserId"` + } `json:"fields"` +} + +type EmbedField struct { + Name string `json:"name"` + Value string `json:"value"` + Inline bool `json:"inline,omitempty"` +} + +type Embed struct { + Author struct { + Name string `json:"name"` + URL string `json:"url"` + IconURL string `json:"icon_url"` + } `json:"author"` + Title string `json:"title"` + URL string `json:"url"` + Description string `json:"description"` + Color int `json:"color"` + Fields []EmbedField `json:"fields"` + Thumbnail struct { + URL string `json:"url"` + } `json:"thumbnail"` + Image struct { + URL string `json:"url"` + } `json:"image"` + Footer struct { + Text string `json:"text"` + IconURL string `json:"icon_url"` + } `json:"footer"` +} + +type WebhookBody struct { + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + Content string `json:"content"` + Embeds []Embed `json:"embeds"` +} + +type DiscordWebhookResponse struct { + Type int `json:"type"` + Content string `json:"content"` + Mentions []any `json:"mentions"` + MentionRoles []any `json:"mention_roles"` + Attachments []any `json:"attachments"` + Embeds []struct { + Type string `json:"type"` + URL string `json:"url"` + Color int `json:"color"` + Fields []struct { + Name string `json:"name"` + Value string `json:"value"` + Inline bool `json:"inline"` + } `json:"fields"` + Thumbnail struct { + URL string `json:"url"` + ProxyURL string `json:"proxy_url"` + Width int `json:"width"` + Height int `json:"height"` + Flags int `json:"flags"` + } `json:"thumbnail"` + } `json:"embeds"` + Timestamp time.Time `json:"timestamp"` + EditedTimestamp any `json:"edited_timestamp"` + Flags int `json:"flags"` + Components []any `json:"components"` + ID string `json:"id"` + ChannelID string `json:"channel_id"` + Author struct { + ID string `json:"id"` + Username string `json:"username"` + Avatar any `json:"avatar"` + Discriminator string `json:"discriminator"` + PublicFlags int `json:"public_flags"` + Flags int `json:"flags"` + Bot bool `json:"bot"` + GlobalName any `json:"global_name"` + Clan any `json:"clan"` + } `json:"author"` + Pinned bool `json:"pinned"` + MentionEveryone bool `json:"mention_everyone"` + Tts bool `json:"tts"` + WebhookID string `json:"webhook_id"` +} + +func (server *httpImpl) SendNotificationToWebhook(webhook string, editing bool, notification db.SharepointNotification) string { + if !editing { + webhook += "?wait=true" + } + + if len([]rune(notification.Description)) > 4096 { + notification.Description = string([]rune(notification.Description)[0:4093]) + "..." + } + + request := req.C().DevMode().R() + + createdOn := time.Unix(int64(notification.CreatedOn), 0) + created := createdOn.Format("02. 01. 2006 ob 15.04") + + modifiedOn := time.Unix(int64(notification.ModifiedOn), 0) + modified := modifiedOn.Format("02. 01. 2006 ob 15.04") + + body := WebhookBody{ + Username: "Intranet", + AvatarURL: "", + Content: "Novo obvestilo na intranetu", + Embeds: []Embed{ + { + Author: struct { + Name string `json:"name"` + URL string `json:"url"` + IconURL string `json:"icon_url"` + }{Name: notification.CreatedBy, URL: "", IconURL: ""}, + Title: notification.Name, + Description: notification.Description, + Color: 15258703, + URL: fmt.Sprintf("https://gimnazijabezigrad.sharepoint.com/Lists/ObvAkt/DispForm.aspx?ID=%s", notification.ID), + Fields: []EmbedField{ + { + Name: "Ustvarjeno", + Value: created, + Inline: true, + }, + { + Name: "Nazadnje spremenjeno", + Value: modified, + Inline: true, + }, + { + Name: "Nazadnje spremenil", + Value: notification.ModifiedBy, + Inline: true, + }, + }, + Thumbnail: struct { + URL string `json:"url"` + }{URL: "https://www.gimb.org/wp-content/uploads/2017/01/logo.png"}, + }, + }, + } + + request.SetBodyJsonMarshal(body) + + var resp *req.Response + var err error + if editing { + resp, err = request.Patch(webhook) + } else { + resp, err = request.Post(webhook) + } + if resp == nil || err != nil { + server.logger.Errorw("failure while sending message to discord webhook", "err", err) + return "" + } + server.logger.Infow("Discord responded with status code", "statusCode", resp.StatusCode) + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { + server.logger.Errorw("error while sending message to Discord", "body", resp.String()) + } + + if editing { + return "" + } + + var unmarshal DiscordWebhookResponse + err = resp.Unmarshal(&unmarshal) + if err != nil { + server.logger.Errorw("could not unmarshal response", "body", resp.String()) + return "" + } + + return unmarshal.ID +} + +func (server *httpImpl) GetSharepointNotificationsGoroutine(accessToken string) { + server.logger.Infow("getting Sharepoint notifications") + + client := req.C() + + client.Headers = make(http.Header) + client.Headers.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + nextLink := "https://graph.microsoft.com/v1.0/sites/root/lists/54521912-06dd-4ccc-8edb-8173c9629fd8/items" + for nextLink != "" { + res, err := client.R().Get(nextLink) + if err != nil { + server.logger.Errorw("error getting all Sharepoint items", "err", err) + break + } + + var response SharepointResponse + err = res.UnmarshalJson(&response) + if err != nil { + server.logger.Errorw("error parsing Microsoft response", "err", err) + break + } + + nextLink = response.OdataNextLink + if nextLink != "" { + server.logger.Infow("got next page on Sharepoint", "page", nextLink) + } + + for _, v := range response.Value { + notificationDb, noterr := server.db.GetSharepointNotification(v.Id) + if (noterr == nil && notificationDb.ModifiedOn == int(v.LastModifiedDateTime.Unix())) || (noterr != nil && !errors.Is(noterr, sql.ErrNoRows)) { + if err != nil { + server.logger.Errorw("error retrieving Sharepoint notification", "id", v.Id, "webUrl", v.WebUrl, "err", err) + } + continue + } + + res, err = client.R().Get(fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/root/lists/54521912-06dd-4ccc-8edb-8173c9629fd8/items/%s", v.Id)) + if err != nil { + server.logger.Errorw("error getting a Sharepoint notification", "id", v.Id, "err", err) + break + } + + var notificationResponse SharepointNotificationResponse + err = res.UnmarshalJson(¬ificationResponse) + if err != nil { + server.logger.Errorw("error parsing Sharepoint notification response", "id", v.Id, "err", err) + break + } + + opt := &md.Options{ + LinkStyle: "referenced", + } + converter := md.NewConverter("", true, opt) + markdown, err := converter.ConvertString(notificationResponse.Fields.Body) + if err != nil { + server.logger.Errorw("error parsing Sharepoint HTML", "err", err) + break + } + notificationResponse.Fields.Body = markdown + + expires := int(notificationResponse.Fields.Expires.Unix()) + if expires < 0 { + expires = 0 + } + + if errors.Is(noterr, sql.ErrNoRows) { + server.logger.Infow("creating new notification", "id", v.Id) + + not := db.SharepointNotification{ + ID: notificationResponse.Id, + Name: notificationResponse.Fields.Title, + Description: notificationResponse.Fields.Body, + CreatedOn: int(notificationResponse.Fields.Created.Unix()), + ModifiedOn: int(notificationResponse.Fields.Modified.Unix()), + CreatedBy: notificationResponse.CreatedBy.User.DisplayName, + ModifiedBy: notificationResponse.LastModifiedBy.User.DisplayName, + MessageIDs: "[]", + ExpiresOn: expires, + HasAttachments: notificationResponse.Fields.Attachments, + } + + ids := make([]string, 0) + for _, webhook := range server.config.Webhooks { + id := server.SendNotificationToWebhook(webhook, false, not) + if id == "" { + continue + } + ids = append(ids, fmt.Sprintf("%s/messages/%s", webhook, id)) + } + + marshal, err := json.Marshal(ids) + if err != nil { + server.logger.Errorw("error while marshalling message IDs", "err", err) + continue + } + not.MessageIDs = string(marshal) + + err = server.db.InsertSharepointNotification(not) + if err != nil { + server.logger.Errorw("error inserting Sharepoint notification", "id", v.Id, "notification", notificationResponse, "not", not, "err", err) + continue + } + } else { + server.logger.Infow("updating an existing notification", "id", v.Id) + + notificationDb.ModifiedOn = int(notificationResponse.Fields.Modified.Unix()) + notificationDb.ModifiedBy = notificationResponse.LastModifiedBy.User.DisplayName + notificationDb.ExpiresOn = expires + notificationDb.Name = notificationResponse.Fields.Title + notificationDb.Description = notificationResponse.Fields.Body + + err := server.db.UpdateSharepointNotification(notificationDb) + if err != nil { + server.logger.Errorw("error updating Sharepoint notification", "id", v.Id, "notification", notificationResponse, "not", notificationDb, "err", err) + continue + } + + var unmarshal []string + err = json.Unmarshal([]byte(notificationDb.MessageIDs), &unmarshal) + if err != nil { + server.logger.Errorw("error unmarshalling message IDs", "err", err) + continue + } + + for _, webhook := range unmarshal { + server.SendNotificationToWebhook(webhook, true, notificationDb) + } + } + } + } +} + +func (server *httpImpl) SharepointGoroutine() { + server.logger.Infow("starting Sharepoint goroutine") + + for { + if server.config.MicrosoftOAUTH2RefreshToken == "" { + server.logger.Infow("no Microsoft OAUTH2 refresh token was found") + server.MicrosoftOAUTH2URL() + server.MicrosoftOAUTH2Callback() + return // konča gorutino, avtomatično znova zažene program + } + + client := req.C() + + body := map[string]string{ + "client_id": server.config.MicrosoftOAUTH2ClientID, + "client_secret": server.config.MicrosoftOAUTH2Secret, + "refresh_token": server.config.MicrosoftOAUTH2RefreshToken, + "scope": SCOPE, + "grant_type": "refresh_token", + } + + res, err := client.R().SetFormData(body).Post("https://login.microsoftonline.com/organizations/oauth2/v2.0/token") + if err != nil { + server.logger.Errorw("error getting token", "err", err) + break + } + + var response MicrosoftOUATH2Response + err = res.UnmarshalJson(&response) + if err != nil { + server.logger.Errorw("error parsing Microsoft response", "err", err) + break + } + + accessToken := response.AccessToken + refreshToken := response.RefreshToken + + server.config.MicrosoftOAUTH2RefreshToken = refreshToken + err = config.SaveConfig(server.config) + if err != nil { + server.logger.Errorw("error saving config", "err", err) + break + } + + server.GetSharepointNotificationsGoroutine(accessToken) + + server.logger.Infow("ran Sharepoint goroutine") + time.Sleep(time.Hour) + } + + server.logger.Infow("exiting Sharepoint goroutine") +} + +func (server *httpImpl) MicrosoftOAUTH2URL() { + fmt.Printf("Obiščite stran in avtorizirajte session: https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize?client_id=%s&response_type=code&response_mode=query&scope=offline_access %s\n", server.config.MicrosoftOAUTH2ClientID, SCOPE) +} + +func (server *httpImpl) MicrosoftOAUTH2Callback() { + reader := bufio.NewReader(os.Stdin) + fmt.Print("Enter Microsoft code: ") + code, err := reader.ReadString('\n') + if err != nil { + server.logger.Fatalw("error reading input", "err", err) + } + + client := req.C() + + body := map[string]string{ + "client_id": server.config.MicrosoftOAUTH2ClientID, + "client_secret": server.config.MicrosoftOAUTH2Secret, + "code": code, + "scope": SCOPE, + "grant_type": "authorization_code", + } + + res, err := client.R().SetFormData(body).Post("https://login.microsoftonline.com/organizations/oauth2/v2.0/token") + if err != nil { + server.logger.Fatalw("error getting token", "err", err) + return + } + + var response MicrosoftOUATH2Response + err = res.UnmarshalJson(&response) + if err != nil { + server.logger.Fatalw("error unmarshalling token", "err", err) + return + } + + server.config.MicrosoftOAUTH2RefreshToken = response.RefreshToken + err = config.SaveConfig(server.config) + if err != nil { + server.logger.Fatalw("error saving token", "err", err) + return + } + + server.logger.Infow("token received successfully") +}