Skip to content

Commit

Permalink
implement Github module
Browse files Browse the repository at this point in the history
  • Loading branch information
kamek-pf committed Jun 18, 2020
1 parent 1a7223d commit fa0ad64
Show file tree
Hide file tree
Showing 33 changed files with 859 additions and 210 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/samples
/test-artefacts
/ntfd
/ntfd-*
.stack-work/
Expand Down
3 changes: 3 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog for ntfd

#### 0.2.0
- Add GitHub module

#### 0.1.0
- Add weather module
- Add mpd module
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ FROM alpine:3.12 as builder

# Dev dependencies
RUN apk --no-cache add --repository http://dl-cdn.alpinelinux.org/alpine/edge/community \
ca-certificates git ghc=8.8.3-r0 upx curl musl-dev gmp-dev zlib-dev zlib-static glib-static pcre-dev libx11-dev libxrandr-dev
ca-certificates git ghc=8.8.3-r0 libressl-dev curl musl-dev gmp-dev zlib-dev zlib-static glib-static pcre-dev libx11-dev libxrandr-dev

# Stack
RUN curl -L https://github.com/commercialhaskell/stack/releases/download/v2.3.1/stack-2.3.1-linux-x86_64-static.tar.gz | tar -xz ; \
Expand Down
47 changes: 41 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ A lightweight notification daemon for fancy desktop integrations.
`ntfd` synchronizes with different services and offers synchronous APIs for desktop integration via D-Bus. \
It can be used as a data source for [Polybar](https://github.com/polybar/polybar), [Rofi](https://github.com/davatorium/rofi) or any other similar tool.

<p align="center">
<img src="./screenshots/main.png" />
</p>


## Installation
Arch users can install [`ntfd-bin`](https://aur.archlinux.org/packages/ntfd-bin/) from the AUR. \
Instructions to build from source can be found at the bottom of the README.
Expand Down Expand Up @@ -85,6 +90,31 @@ font-2 = Weather Icons:size=12;0
...
```

## GitHub module
<p align="center">
<img width="38%" src="./screenshots/github-notification.png" />
</p>
<p align="center">
<img src="./screenshots/github-polybar.png" />
</p>

The GitHub module sends desktop notifications when there's activity on GitHub. \
It exposes a D-Bus similar to the weather module.

#### Polybar integration
The screenshots shows the number of currently unread notifications . \
Edit the `~/.config/ntfd/config.toml` and follow the instructions. \
For Polybar integration like in the example, update your Polybar config like so:
```
[module/github]
type = custom/script
exec = busctl --user -j get-property io.ntfd /github github.strings RenderedTemplate | jq -r .data
interval = 10
label-font = 3
```
I recommend a 10 second interval, this way the bar will stay in sync with the notifications. \
The example in the default config file needs the Octicons font to render correctly.

## MPD module
<p align="center">
<img width="30%" src="./screenshots/mpd-notification.png" />
Expand All @@ -102,18 +132,20 @@ Integration with the following services is planned:
- [x] Alerts through notifications
- [x] MPD
- [x] Desktop notifications
- [ ] Github
- [ ] Unread notifications count
- [ ] Live notifications (?)
- [ ] Twitch
- [ ] Live streams count (followed by the user)
- [ ] Rofi integration with [`mpv`](https://mpv.io/)
- [x] Github
- [x] Unread notifications count
- [x] Live notifications
- [ ] Arch
- [ ] Pacman updates ? (how ?)
- [ ] Gmail
- [ ] Live notifications
- [ ] Unread messages count, multi account support
- [ ] Facebook (?)
- [ ] Live messages (?)
- [ ] Unread notifications count (?)
- [ ] Twitch
- [ ] Live streams count (followed by the user)
- [ ] Rofi integration with [`mpv`](https://mpv.io/)
- [ ] Reddit (?)

## Build from source
Expand All @@ -124,6 +156,9 @@ docker run --rm -ti -v $(pwd):/mnt kamek-pf/ntfd /bin/sh -c 'cp ntfd /mnt'
```
The binary will be available as `ntfd` from the project's root.

## Run tests
The test suite expects a valid `OWM_API_KEY` and `GITHUB_TOKEN` environment variables. Simply run `stack test`.

## Troubleshooting

##### My Dunst notification icons look tiny
Expand Down
5 changes: 3 additions & 2 deletions app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import System.Directory (getXdgDirectory, XdgDirectory(..))
import System.Exit (exitFailure)

import Config (loadConfig, Config(..))
import Modules.Github (githubStringsSvc)
import Modules.Weather (weatherStringsSvc)
import Modules.Mpd (mpdNotifSvc)

Expand All @@ -33,10 +34,10 @@ main = do
exitFailure

-- Prepare services
let githubSvc = githubStringsSvc client <$> githubCfg config
let weatherSvc = weatherStringsSvc client <$> weatherCfg config
let mpdSvc = mpdNotifSvc client <$> mpdCfg config
let allServices = [weatherSvc, mpdSvc]

let allServices = [githubSvc, weatherSvc, mpdSvc]
-- Log which services failed to initialize / are disabled
-- print $ lefts allServices

Expand Down
69 changes: 69 additions & 0 deletions app/Modules/Github.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module Modules.Github
( githubStringsSvc
)
where

import Control.Concurrent.Async (mapConcurrently_)
import Control.Monad (forever)
import Data.Either (fromRight)
import Data.Int (Int32)
import Data.Text (pack)
import Data.Time.Clock (secondsToNominalDiffTime)
import Data.Word (Word32)
import DBus.Client
(defaultInterface, export, readOnlyProperty, interfaceName, interfaceProperties, Client)
import System.Random (randomRIO)

import Config (GithubConfig(..))
import Helpers (notify, sleep, NotificationType(..))
import qualified Stores.Github as GH

-- "all strings" version of the github DBus API, this is meant to be convenient to use
-- from shell scripts, errors are mapped to empty strings (since DBus has no null/empty type)
githubStringsSvc :: Client -> GithubConfig -> IO ()
githubStringsSvc dbusClient config = do
putStrLn "Started Github synchronization service."
(store :: GH.GithubClient) <- GH.initGithubStore config
export dbusClient "/github" $ interface store
forever $ do
syncRes <- GH.syncGithub store
case syncRes of
Right notifyIds -> do
ns <- filterNew store notifyIds
sendNotifications ns
if not $ null notifyIds
then sleep $ githubSyncFreq config - delay
else sleep $ githubSyncFreq config

_ -> sleep $ githubSyncFreq config
where
delay = secondsToNominalDiffTime 15 -- Keep notifications in sync with template
interface s =
defaultInterface { interfaceName = "github.strings", interfaceProperties = properties s }
properties s =
[ readOnlyProperty "RenderedTemplate" $ fromRight "" <$> GH.getRenderedTemplate s
, readOnlyProperty "FirstTimeNotifications" $ toInt (GH.getFistTimeNotificationCount s)
, readOnlyProperty "UnreadNotifications" $ toInt (GH.getUnreadNotificationCount s)
]
toInt value = do
maybeNatural <- value
pure $ maybe 0 fromIntegral maybeNatural :: IO Int32
filterNew store newIds = do
ns <- GH.getNotifications store
pure $ filter (isNewNotification newIds) ns
isNewNotification ns n =
let notifId = (GH.notificationId . GH.restNotification) n in elem notifId ns
sendNotifications = mapConcurrently_ sendNotification
sendNotification n = do
rid <- randomRIO (50, maxBound :: Word32)
let timeout = githubNotifTime config
let nType = pack . show $ (GH.notificationType . GH.restNotification) n
let description = (GH.title . GH.restNotification) n
let title = (GH.repoFullName . GH.restNotification) n
let
body = if (GH.isNew . GH.restNotification) n
then "New GitHub " <> nType <> "\n\n" <> description
else "Activity in GitHub " <> nType <> "\n\n" <> description
let icon = if githubShowAvatar config then Just $ GH.avatarPath n else Nothing
sleep delay
notify dbusClient (Github rid) title body icon timeout
9 changes: 4 additions & 5 deletions app/Modules/Mpd.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import Network.MPD
import System.Directory (doesFileExist)
import System.FilePath (joinPath, splitFileName)
import qualified Data.Map as M
import qualified Data.Text as T

import Helpers (notify, NotificationType(..))
import Config (GlobalConfig(..), MpdConfig(..))
import Config (MpdConfig(..))

-- MPD notification service, watches player state changes and sends
-- notifications on track change
Expand Down Expand Up @@ -51,13 +50,13 @@ mpdNotifSvc client config = do
(Just [title], Just [artist], Just [album], _) -> do
let nHead = toText title
let nBody = toText artist <> " - " <> toText album
let nTimeout = (notificationTimeout . mpdGlobalCfg) config
let nTimeout = mpdNotifTime config
when (shouldNotify cover) $ notify client Mpd nHead nBody cover nTimeout
-- Streaming content
(Just [title], _, _, Just [name]) -> do
let nHead = toText title
let nBody = toText name
let nTimeout = (notificationTimeout . mpdGlobalCfg) config
let nTimeout = mpdNotifTime config
when (shouldNotify cover) $ notify client Mpd nHead nBody cover nTimeout
_ -> pure ()
getCoverPath song = do
Expand All @@ -66,7 +65,7 @@ mpdNotifSvc client config = do
let (songDir, _) = splitFileName $ (toString . sgFilePath) song
let coverPath = joinPath [musicDir, songDir, coverFile]
hasCover <- doesFileExist coverPath
pure $ if hasCover then Just (T.pack coverPath) else Nothing
pure $ if hasCover then Just coverPath else Nothing
shouldNotify coverPath =
let
hasCover = isJust coverPath
Expand Down
21 changes: 12 additions & 9 deletions app/Modules/Weather.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ module Modules.Weather
where

import Control.Monad (forever)
import Data.Maybe (fromMaybe)
import Data.Either (fromRight)
import Data.Time.Clock (secondsToNominalDiffTime)
import Data.Text (Text)
import Data.Text (unpack, Text)
import DBus.Client
( autoMethod
, defaultInterface
Expand All @@ -17,9 +19,9 @@ import DBus.Client
, Client
)

import Config (GlobalConfig(..), WeatherConfig(..))
import Config (WeatherConfig(..))
import Types.Weather (convert, Temperature(..), Unit(..))
import Helpers (capitalize, notify, sleep, fromEither, fromMaybe, NotificationType(..))
import Helpers (capitalize, notify, sleep, NotificationType(..))
import qualified Stores.Weather as WS

-- "all strings" version of the weather DBus API, this is meant to be convenient to use
Expand All @@ -34,21 +36,22 @@ weatherStringsSvc dbusClient config = do
case shouldNotify of
Right True -> do
let body = weatherNotifBody config
let timeout = (notificationTimeout . weatherGlobalCfg) config
title <- capitalize . fromMaybe <$> WS.getForecastDescription store
let timeout = weatherNotifTime config
title <- capitalize . fromMaybe "" <$> WS.getForecastDescription store
icon <- WS.getForecastSymbolic store
sleep delay
notify dbusClient Weather title body icon timeout
_ -> pure ()
sleep $ weatherSyncFreq config - delay
notify dbusClient Weather title body (unpack <$> icon) timeout
sleep $ weatherSyncFreq config - delay

_ -> sleep $ weatherSyncFreq config
where
interface s = defaultInterface
{ interfaceName = "openweathermap.strings"
, interfaceMethods = methods s
, interfaceProperties = properties s
}
renderedTemplate s =
readOnlyProperty "RenderedTemplate" $ fromEither <$> WS.getRenderedTemplate s
readOnlyProperty "RenderedTemplate" $ fromRight "" <$> WS.getRenderedTemplate s
currentIcon s = readOnlyProperty "CurrentIcon" $ fromIcon <$> WS.getCurrentIcon s
forecastIcon s = readOnlyProperty "ForecastIcon" $ fromIcon <$> WS.getForecastIcon s
currentTemp s = autoMethod "CurrentTemperature" $ currentTemperature s
Expand Down
51 changes: 33 additions & 18 deletions config.toml
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
# Settings that apply to all modules
[global]
# How long notifications should stay up (in seconds)
notification_timeout = 10

# OpenWeatherMap configuration
[openweathermap]
# Enable / disable OpenWeatherMap integration
enabled = false

# Register at https://openweathermap.org to get your API key
# If you don't want to write your key here, use one of the alternate forms.
# To read from a file or an environment variable:
# api_key = "file:~/your_file/some-key.txt"
# api_key = "env:OWM_API_KEY"
api_key = "YOUR_API_KEY"

# Use one of the following forms to set your API key:
# api_key = "file:~/your_folder/some-key.txt" # Read from a file
# api_key = "env:OWM_API_KEY" # Read from an environment variable
# api_key = "PLAINTEXT_VALUE" # Read the value directly
api_key = "file:~/.config/ntfd/openweathermap.txt"
# This is for Montreal, find your city at https://openweathermap.org
# The id will be the last part of the URL
city_id = "6077243"

# How long weather notifications should stay up (in seconds)
notification_timeout = 10
# When notifications are sent, the notification title comes from OpenWeatherMap
# You can customize the message body, or use "" to leave it blank
notification_body = "Expected within the next 3 hours"

# How often should we fetch weather data (in seconds)
# Will default to 10 min for values under 10 min (600 seconds)
sync_frequency = 1800 # 30 minutes

# Output format, using Handlebars syntax, meaning variables should be used like {{ this }}
# Available tokens are:
# - temp_celcius
Expand All @@ -40,19 +32,42 @@ sync_frequency = 1800 # 30 minutes
# - forecast_icon
display = "{{ temp_icon }} {{ temp_celcius }}°C {{ trend }} {{ forecast_icon }} {{ forecast_celcius }}°C"

# GitHub configuration
[github]
# Enable / disable GitHub integration
enabled = false
# Go to https://github.com/settings/tokens
# Click on "Generate a new token", give it a name and check the "notifications" checkbox
# Use one of the following forms to set your token:
# api_key = "file:~/your_folder/some-token.txt" # Read from a file
# api_key = "env:GITHUB_TOKEN" # Read from an environment variable
# api_key = "PLAINTEXT_VALUE" # Read the value directly
api_key = "file:~/.config/ntfd/github.txt"
# How long GitHub notifications should stay up (in seconds)
notification_timeout = 20
# Whether or not the organization/owner avatar should be shown in the notification
show_avatar = true
# How often should we fetch from the GitHub API (in seconds)
# Will default to 10 seconds for values under 10 seconds
sync_frequency = 30
# Output format, using Handlebars syntax, meaning variables should be used like {{ this }}
# Available tokens are:
# - unread_count
# - first_time_count
display = " {{ unread_count }}"

# MPD configuration
[mpd]
# Enable / disable mpd integration
enabled = false

# Top level directory containing your MPD collection
# Should be the same value as "music_directory" from your mpd.conf
music_directory = "/mnt/media/music"

# How long MPD notifications should stay up (in seconds)
notification_timeout = 10
# Cover art files should have standardized names and formats within your collection
# Specify the file name we should look for here
cover_name = "cover.jpg"

# When this is enabled, songs with missing cover image will not trigger a notification
skip_missing_cover = true

Expand Down
Loading

0 comments on commit fa0ad64

Please sign in to comment.