Implement part of a push-based notification system
This commit is contained in:
parent
c3c2036c69
commit
4ef8471585
13
src/Main.elm
13
src/Main.elm
|
@ -5,6 +5,7 @@ import Scylla.Room exposing (OpenRooms, applySync)
|
||||||
import Scylla.Sync exposing (..)
|
import Scylla.Sync exposing (..)
|
||||||
import Scylla.Sync.Events exposing (toMessageEvent, getType, getSender, getUnsigned)
|
import Scylla.Sync.Events exposing (toMessageEvent, getType, getSender, getUnsigned)
|
||||||
import Scylla.Sync.AccountData exposing (..)
|
import Scylla.Sync.AccountData exposing (..)
|
||||||
|
import Scylla.Sync.Push exposing (..)
|
||||||
import Scylla.ListUtils exposing (..)
|
import Scylla.ListUtils exposing (..)
|
||||||
import Scylla.Messages exposing (..)
|
import Scylla.Messages exposing (..)
|
||||||
import Scylla.Login exposing (..)
|
import Scylla.Login exposing (..)
|
||||||
|
@ -51,7 +52,6 @@ init _ url key =
|
||||||
, sending = Dict.empty
|
, sending = Dict.empty
|
||||||
, transactionId = 0
|
, transactionId = 0
|
||||||
, userData = Dict.empty
|
, userData = Dict.empty
|
||||||
, roomNames = Dict.empty
|
|
||||||
, connected = True
|
, connected = True
|
||||||
, searchText = ""
|
, searchText = ""
|
||||||
, rooms = emptyOpenRooms
|
, rooms = emptyOpenRooms
|
||||||
|
@ -314,10 +314,13 @@ updateSyncResponse model r notify =
|
||||||
userDataCmd sr = newUsersCmd model
|
userDataCmd sr = newUsersCmd model
|
||||||
<| newUsers model
|
<| newUsers model
|
||||||
<| allUsers sr
|
<| allUsers sr
|
||||||
notification sr = findFirstBy
|
notification sr =
|
||||||
(\(s, e) -> e.originServerTs)
|
getPushRuleset model.accountData
|
||||||
(\(s, e) -> e.sender /= model.loginUsername)
|
|> Maybe.map (\rs -> getNotificationEvents rs sr)
|
||||||
<| getNotificationEvents sr
|
|> Maybe.withDefault []
|
||||||
|
|> findFirstBy
|
||||||
|
(\(s, e) -> e.originServerTs)
|
||||||
|
(\(s, e) -> e.sender /= model.loginUsername)
|
||||||
notificationCmd sr = if notify
|
notificationCmd sr = if notify
|
||||||
then Maybe.withDefault Cmd.none
|
then Maybe.withDefault Cmd.none
|
||||||
<| Maybe.map (\(s, e) -> sendNotificationPort
|
<| Maybe.map (\(s, e) -> sendNotificationPort
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Scylla.Sync exposing (SyncResponse, HistoryResponse)
|
||||||
import Scylla.ListUtils exposing (findFirst)
|
import Scylla.ListUtils exposing (findFirst)
|
||||||
import Scylla.Room exposing (OpenRooms)
|
import Scylla.Room exposing (OpenRooms)
|
||||||
import Scylla.Sync.Rooms exposing (JoinedRoom)
|
import Scylla.Sync.Rooms exposing (JoinedRoom)
|
||||||
|
import Scylla.Sync.Push exposing (Ruleset)
|
||||||
import Scylla.Sync.AccountData exposing (AccountData, directMessagesDecoder)
|
import Scylla.Sync.AccountData exposing (AccountData, directMessagesDecoder)
|
||||||
import Scylla.Login exposing (LoginResponse, Username, Password)
|
import Scylla.Login exposing (LoginResponse, Username, Password)
|
||||||
import Scylla.UserData exposing (UserData)
|
import Scylla.UserData exposing (UserData)
|
||||||
|
@ -36,7 +37,6 @@ type alias Model =
|
||||||
, sending : Dict Int (RoomId, SendingMessage)
|
, sending : Dict Int (RoomId, SendingMessage)
|
||||||
, transactionId : Int
|
, transactionId : Int
|
||||||
, userData : Dict Username UserData
|
, userData : Dict Username UserData
|
||||||
, roomNames : Dict RoomId String
|
|
||||||
, connected : Bool
|
, connected : Bool
|
||||||
, searchText : String
|
, searchText : String
|
||||||
, rooms : OpenRooms
|
, rooms : OpenRooms
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
port module Scylla.Notification exposing (..)
|
port module Scylla.Notification exposing (..)
|
||||||
import Scylla.Sync exposing (SyncResponse, joinedRoomsTimelineEvents)
|
import Scylla.Sync exposing (SyncResponse, joinedRoomsTimelineEvents)
|
||||||
import Scylla.Sync.Events exposing (RoomEvent, MessageEvent, toMessageEvent)
|
import Scylla.Sync.Events exposing (RoomEvent, MessageEvent, toMessageEvent)
|
||||||
|
import Scylla.Sync.Push exposing (Ruleset, getEventNotification)
|
||||||
import Json.Decode as Decode exposing (string, field)
|
import Json.Decode as Decode exposing (string, field)
|
||||||
import Dict
|
import Dict
|
||||||
|
|
||||||
|
@ -18,13 +19,14 @@ getText re = case (Decode.decodeValue (field "msgtype" string) re.content) of
|
||||||
Ok "m.text" -> Result.withDefault "" <| (Decode.decodeValue (field "body" string) re.content)
|
Ok "m.text" -> Result.withDefault "" <| (Decode.decodeValue (field "body" string) re.content)
|
||||||
_ -> ""
|
_ -> ""
|
||||||
|
|
||||||
getNotificationEvents : SyncResponse -> List (String, MessageEvent)
|
getNotificationEvents : Ruleset -> SyncResponse -> List (String, MessageEvent)
|
||||||
getNotificationEvents s =
|
getNotificationEvents rs s = s.rooms
|
||||||
let
|
|> Maybe.andThen .join
|
||||||
applyPair k = List.map (\v -> (k, v))
|
|> Maybe.map (Dict.map (\k v -> v.timeline
|
||||||
in
|
|> Maybe.andThen .events
|
||||||
List.sortBy (\(k, v) -> v.originServerTs)
|
|> Maybe.map (List.filter <| getEventNotification rs k)
|
||||||
<| List.filterMap (\(k, e) -> Maybe.map (\me -> (k, me)) <| toMessageEvent e)
|
|> Maybe.map (List.filterMap <| toMessageEvent)
|
||||||
<| Dict.foldl (\k v a -> a ++ applyPair k v) []
|
|> Maybe.withDefault []))
|
||||||
<| joinedRoomsTimelineEvents s
|
|> Maybe.withDefault Dict.empty
|
||||||
|
|> Dict.toList
|
||||||
|
|> List.concatMap (\(k, vs) -> List.map (\v -> (k, v)) vs)
|
||||||
|
|
|
@ -2,7 +2,8 @@ module Scylla.Sync.AccountData exposing (..)
|
||||||
import Scylla.ListUtils exposing (..)
|
import Scylla.ListUtils exposing (..)
|
||||||
import Scylla.Sync.DecodeTools exposing (maybeDecode)
|
import Scylla.Sync.DecodeTools exposing (maybeDecode)
|
||||||
import Scylla.Sync.Events exposing (Event, eventDecoder)
|
import Scylla.Sync.Events exposing (Event, eventDecoder)
|
||||||
import Json.Decode as Decode exposing (Decoder, list, decodeValue)
|
import Scylla.Sync.Push exposing (Ruleset, rulesetDecoder)
|
||||||
|
import Json.Decode as Decode exposing (Decoder, list, field, decodeValue)
|
||||||
import Dict exposing (Dict)
|
import Dict exposing (Dict)
|
||||||
|
|
||||||
type alias AccountData =
|
type alias AccountData =
|
||||||
|
@ -48,3 +49,6 @@ getAccountData key d ad = ad.events
|
||||||
|
|
||||||
getDirectMessages : AccountData -> Maybe DirectMessages
|
getDirectMessages : AccountData -> Maybe DirectMessages
|
||||||
getDirectMessages = getAccountData "m.direct" directMessagesDecoder
|
getDirectMessages = getAccountData "m.direct" directMessagesDecoder
|
||||||
|
|
||||||
|
getPushRuleset : AccountData -> Maybe Ruleset
|
||||||
|
getPushRuleset = getAccountData "m.push_rules" (field "global" rulesetDecoder)
|
||||||
|
|
|
@ -124,6 +124,12 @@ getType re =
|
||||||
StateRoomEvent e -> e.type_
|
StateRoomEvent e -> e.type_
|
||||||
MessageRoomEvent e -> e.type_
|
MessageRoomEvent e -> e.type_
|
||||||
|
|
||||||
|
getContent : RoomEvent -> Decode.Value
|
||||||
|
getContent re =
|
||||||
|
case re of
|
||||||
|
StateRoomEvent e -> e.content
|
||||||
|
MessageRoomEvent e -> e.content
|
||||||
|
|
||||||
toStateEvent : RoomEvent -> Maybe StateEvent
|
toStateEvent : RoomEvent -> Maybe StateEvent
|
||||||
toStateEvent re =
|
toStateEvent re =
|
||||||
case re of
|
case re of
|
||||||
|
|
167
src/Scylla/Sync/Push.elm
Normal file
167
src/Scylla/Sync/Push.elm
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
module Scylla.Sync.Push exposing (..)
|
||||||
|
import Scylla.Sync.DecodeTools exposing (maybeDecode)
|
||||||
|
import Scylla.Sync.Events exposing (RoomEvent, getSender, getContent, getType)
|
||||||
|
import Scylla.Route exposing (RoomId)
|
||||||
|
import Json.Decode as Decode exposing (Decoder, string, int, field, value, bool, list)
|
||||||
|
import Json.Decode.Pipeline exposing (required, optional)
|
||||||
|
|
||||||
|
type Condition
|
||||||
|
= EventMatch String String
|
||||||
|
| ContainsDisplayName
|
||||||
|
| RoomMemberCount Int
|
||||||
|
| SenderNotificationPermission String
|
||||||
|
|
||||||
|
conditionDecoder : Decoder Condition
|
||||||
|
conditionDecoder =
|
||||||
|
let
|
||||||
|
eventMatchDecoder =
|
||||||
|
Decode.succeed EventMatch
|
||||||
|
|> required "key" string
|
||||||
|
|> required "pattern" string
|
||||||
|
containsDisplayNameDecoder =
|
||||||
|
Decode.succeed ContainsDisplayName
|
||||||
|
roomMemberCountDecoder =
|
||||||
|
Decode.succeed RoomMemberCount
|
||||||
|
|> required "is"
|
||||||
|
(Decode.map (Maybe.withDefault 0 << String.toInt) string)
|
||||||
|
senderNotifPermissionDecoder =
|
||||||
|
Decode.succeed SenderNotificationPermission
|
||||||
|
|> required "key" string
|
||||||
|
dispatchDecoder k =
|
||||||
|
case k of
|
||||||
|
"event_match" -> eventMatchDecoder
|
||||||
|
"contains_display_name" -> containsDisplayNameDecoder
|
||||||
|
"room_member_count" -> roomMemberCountDecoder
|
||||||
|
"sender_notification_permission" -> senderNotifPermissionDecoder
|
||||||
|
_ -> Decode.fail "Unknown condition code"
|
||||||
|
in
|
||||||
|
field "kind" string
|
||||||
|
|> Decode.andThen dispatchDecoder
|
||||||
|
|
||||||
|
type Action
|
||||||
|
= Notify
|
||||||
|
| DontNotify
|
||||||
|
| Coalesce
|
||||||
|
| SetTweak String (Maybe Decode.Value)
|
||||||
|
|
||||||
|
actionDecoder : Decoder Action
|
||||||
|
actionDecoder =
|
||||||
|
let
|
||||||
|
dispatchStringDecoder s =
|
||||||
|
case s of
|
||||||
|
"notify" -> Decode.succeed Notify
|
||||||
|
"dont_notify" -> Decode.succeed DontNotify
|
||||||
|
"coalesce" -> Decode.succeed Coalesce
|
||||||
|
_ -> Decode.fail "Unknown action string"
|
||||||
|
objectDecoder =
|
||||||
|
Decode.succeed SetTweak
|
||||||
|
|> required "set_tweak" string
|
||||||
|
|> maybeDecode "value" value
|
||||||
|
in
|
||||||
|
Decode.oneOf
|
||||||
|
[ string |> Decode.andThen dispatchStringDecoder
|
||||||
|
, objectDecoder
|
||||||
|
]
|
||||||
|
|
||||||
|
type alias Rule =
|
||||||
|
{ ruleId : String
|
||||||
|
, default : Bool
|
||||||
|
, enabled : Bool
|
||||||
|
, conditions : List Condition
|
||||||
|
, actions : List Action
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleDecoder : Decoder Rule
|
||||||
|
ruleDecoder =
|
||||||
|
let
|
||||||
|
patternDecoder = Decode.oneOf
|
||||||
|
[ field "pattern" string
|
||||||
|
|> Decode.andThen
|
||||||
|
(\p -> Decode.succeed <| \r ->
|
||||||
|
{ r | conditions = (EventMatch "content.body" p)::r.conditions })
|
||||||
|
, Decode.succeed identity
|
||||||
|
]
|
||||||
|
basicRuleDecoder = Decode.succeed Rule
|
||||||
|
|> required "rule_id" string
|
||||||
|
|> optional "default" bool True
|
||||||
|
|> optional "enabled" bool False
|
||||||
|
|> optional "conditions" (list conditionDecoder) []
|
||||||
|
|> required "actions" (list actionDecoder)
|
||||||
|
in
|
||||||
|
patternDecoder
|
||||||
|
|> Decode.andThen (\f -> Decode.map f basicRuleDecoder)
|
||||||
|
|
||||||
|
type alias Ruleset =
|
||||||
|
{ content : List Rule
|
||||||
|
, override : List Rule
|
||||||
|
, room : List Rule
|
||||||
|
, sender : List Rule
|
||||||
|
, underride : List Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesetDecoder : Decoder Ruleset
|
||||||
|
rulesetDecoder = Decode.succeed Ruleset
|
||||||
|
|> optional "content" (list ruleDecoder) []
|
||||||
|
|> optional "override" (list ruleDecoder) []
|
||||||
|
|> optional "room" (list ruleDecoder) []
|
||||||
|
|> optional "sender" (list ruleDecoder) []
|
||||||
|
|> optional "underride" (list ruleDecoder) []
|
||||||
|
|
||||||
|
checkCondition : RoomEvent -> Condition -> Bool
|
||||||
|
checkCondition re c =
|
||||||
|
let
|
||||||
|
pathDecoder xs p =
|
||||||
|
Decode.at xs string
|
||||||
|
|> Decode.map (String.contains p << String.toLower)
|
||||||
|
matchesPattern xs p =
|
||||||
|
case Decode.decodeValue (pathDecoder xs p) (getContent re) of
|
||||||
|
Ok True -> True
|
||||||
|
_ -> False
|
||||||
|
in
|
||||||
|
case c of
|
||||||
|
EventMatch k p ->
|
||||||
|
case String.split "." k of
|
||||||
|
"content"::xs -> matchesPattern xs p
|
||||||
|
"type"::[] -> String.contains p <| getType re
|
||||||
|
_ -> False
|
||||||
|
ContainsDisplayName -> False
|
||||||
|
RoomMemberCount _ -> False
|
||||||
|
SenderNotificationPermission _ -> False
|
||||||
|
|
||||||
|
applyAction : Action -> List Action -> List Action
|
||||||
|
applyAction a as_ =
|
||||||
|
case a of
|
||||||
|
Notify -> Notify :: List.filter (\a_ -> a_ /= DontNotify) as_
|
||||||
|
DontNotify -> DontNotify :: List.filter (\a_ -> a_ /= Notify) as_
|
||||||
|
Coalesce -> Coalesce :: List.filter (\a_ -> a_ /= DontNotify) as_
|
||||||
|
a_ -> a_ :: as_
|
||||||
|
|
||||||
|
applyActions : List Action -> List Action -> List Action
|
||||||
|
applyActions l r = List.foldl applyAction r l
|
||||||
|
|
||||||
|
updatePushRuleActions : Rule -> RoomEvent -> List Action -> List Action
|
||||||
|
updatePushRuleActions r re as_ =
|
||||||
|
if List.all (checkCondition re) r.conditions
|
||||||
|
then applyActions r.actions as_
|
||||||
|
else as_
|
||||||
|
|
||||||
|
updatePushActions : List Rule -> RoomEvent -> List Action -> List Action
|
||||||
|
updatePushActions rs re as_ =
|
||||||
|
List.filter .enabled rs
|
||||||
|
|> List.foldl (\r -> updatePushRuleActions r re) as_
|
||||||
|
|
||||||
|
getPushActions : Ruleset -> RoomId -> RoomEvent -> List Action
|
||||||
|
getPushActions rs rid re =
|
||||||
|
let
|
||||||
|
roomRules = List.filter (((==) rid) << .ruleId) rs.room
|
||||||
|
senderRules = List.filter (((==) <| getSender re) << .ruleId) rs.sender
|
||||||
|
in
|
||||||
|
updatePushActions rs.underride re []
|
||||||
|
|> updatePushActions senderRules re
|
||||||
|
|> updatePushActions roomRules re
|
||||||
|
|> updatePushActions rs.override re
|
||||||
|
|
||||||
|
getEventNotification : Ruleset -> RoomId -> RoomEvent -> Bool
|
||||||
|
getEventNotification rs rid re =
|
||||||
|
getPushActions rs rid re
|
||||||
|
|> List.member Notify
|
Loading…
Reference in New Issue
Block a user