386 lines
12 KiB
Go
386 lines
12 KiB
Go
/*
|
|
*
|
|
* Gosora Alerts System
|
|
* Copyright Azareal 2017 - 2020
|
|
*
|
|
*/
|
|
package common
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
//"fmt"
|
|
|
|
"git.tuxpa.in/a/gosora/common/phrases"
|
|
qgen "git.tuxpa.in/a/gosora/query_gen"
|
|
)
|
|
|
|
type Alert struct {
|
|
ASID int
|
|
ActorID int
|
|
TargetUserID int
|
|
Event string
|
|
ElementType string
|
|
ElementID int
|
|
CreatedAt time.Time
|
|
Extra string
|
|
|
|
Actor *User
|
|
}
|
|
|
|
type AlertStmts struct {
|
|
notifyWatchers *sql.Stmt
|
|
getWatchers *sql.Stmt
|
|
}
|
|
|
|
var alertStmts AlertStmts
|
|
|
|
// TODO: Move these statements into some sort of activity abstraction
|
|
// TODO: Rewrite the alerts logic
|
|
func init() {
|
|
DbInits.Add(func(acc *qgen.Accumulator) error {
|
|
alertStmts = AlertStmts{
|
|
notifyWatchers: acc.SimpleInsertInnerJoin(
|
|
qgen.DBInsert{"activity_stream_matches", "watcher,asid", ""},
|
|
qgen.DBJoin{"activity_stream", "activity_subscriptions", "activity_subscriptions.user, activity_stream.asid", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid=?", "", ""},
|
|
),
|
|
getWatchers: acc.SimpleInnerJoin("activity_stream", "activity_subscriptions", "activity_subscriptions.user", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid=?", "", ""),
|
|
}
|
|
return acc.FirstError()
|
|
})
|
|
}
|
|
|
|
const AlertsGrowHint = len(`{"msgs":[],"count":,"tc":}`) + 1 + 10
|
|
|
|
// TODO: See if we can json.Marshal instead?
|
|
func escapeTextInJson(in string) string {
|
|
in = strings.Replace(in, "\"", "\\\"", -1)
|
|
return strings.Replace(in, "/", "\\/", -1)
|
|
}
|
|
|
|
func BuildAlert(a Alert, user User /* The current user */) (out string, err error) {
|
|
var targetUser *User
|
|
if a.Actor == nil {
|
|
a.Actor, err = Users.Get(a.ActorID)
|
|
if err != nil {
|
|
return "", errors.New(phrases.GetErrorPhrase("alerts_no_actor"))
|
|
}
|
|
}
|
|
|
|
/*if a.ElementType != "forum" {
|
|
targetUser, err = users.Get(a.TargetUserID)
|
|
if err != nil {
|
|
LocalErrorJS("Unable to find the target user",w,r)
|
|
return
|
|
}
|
|
}*/
|
|
if a.Event == "friend_invite" {
|
|
return buildAlertString(".new_friend_invite", []string{a.Actor.Name}, a.Actor.Link, a.Actor.Avatar, a.ASID), nil
|
|
}
|
|
|
|
// Not that many events for us to handle in a forum
|
|
if a.ElementType == "forum" {
|
|
if a.Event == "reply" {
|
|
topic, err := Topics.Get(a.ElementID)
|
|
if err != nil {
|
|
DebugLogf("Unable to find linked topic %d", a.ElementID)
|
|
return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic"))
|
|
}
|
|
// Store the forum ID in the targetUser column instead of making a new one? o.O
|
|
// Add an additional column for extra information later on when we add the ability to link directly to posts. We don't need the forum data for now...
|
|
return buildAlertString(".forum_new_topic", []string{a.Actor.Name, topic.Title}, topic.Link, a.Actor.Avatar, a.ASID), nil
|
|
}
|
|
return buildAlertString(".forum_unknown_action", []string{a.Actor.Name}, "", a.Actor.Avatar, a.ASID), nil
|
|
}
|
|
|
|
var url, area, phraseName string
|
|
own := false
|
|
// TODO: Avoid loading a bit of data twice
|
|
switch a.ElementType {
|
|
case "convo":
|
|
convo, err := Convos.Get(a.ElementID)
|
|
if err != nil {
|
|
DebugLogf("Unable to find linked convo %d", a.ElementID)
|
|
return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_convo"))
|
|
}
|
|
url = convo.Link
|
|
case "topic":
|
|
topic, err := Topics.Get(a.ElementID)
|
|
if err != nil {
|
|
DebugLogf("Unable to find linked topic %d", a.ElementID)
|
|
return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic"))
|
|
}
|
|
url = topic.Link
|
|
area = topic.Title
|
|
own = a.TargetUserID == user.ID
|
|
case "user":
|
|
targetUser, err = Users.Get(a.ElementID)
|
|
if err != nil {
|
|
DebugLogf("Unable to find target user %d", a.ElementID)
|
|
return "", errors.New(phrases.GetErrorPhrase("alerts_no_target_user"))
|
|
}
|
|
area = targetUser.Name
|
|
url = targetUser.Link
|
|
own = a.TargetUserID == user.ID
|
|
case "post":
|
|
topic, err := TopicByReplyID(a.ElementID)
|
|
if err != nil {
|
|
DebugLogf("Unable to find linked topic by reply ID %d", a.ElementID)
|
|
return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic_by_reply"))
|
|
}
|
|
url = topic.Link
|
|
area = topic.Title
|
|
own = a.TargetUserID == user.ID
|
|
default:
|
|
return "", errors.New(phrases.GetErrorPhrase("alerts_invalid_elementtype"))
|
|
}
|
|
|
|
badEv := false
|
|
switch a.Event {
|
|
case "create", "like", "mention", "reply":
|
|
// skip
|
|
default:
|
|
badEv = true
|
|
}
|
|
|
|
if own && !badEv {
|
|
phraseName = "." + a.ElementType + "_own_" + a.Event
|
|
} else if !badEv {
|
|
phraseName = "." + a.ElementType + "_" + a.Event
|
|
} else if own {
|
|
phraseName = "." + a.ElementType + "_own"
|
|
} else {
|
|
phraseName = "." + a.ElementType
|
|
}
|
|
|
|
return buildAlertString(phraseName, []string{a.Actor.Name, area}, url, a.Actor.Avatar, a.ASID), nil
|
|
}
|
|
|
|
func buildAlertString(msg string, sub []string, path, avatar string, asid int) string {
|
|
var sb strings.Builder
|
|
buildAlertSb(&sb, msg, sub, path, avatar, asid)
|
|
return sb.String()
|
|
}
|
|
|
|
const AlertsGrowHint2 = len(`{"msg":"","sub":[],"path":"","img":"","id":}`) + 5 + 3 + 1 + 1 + 1
|
|
|
|
// TODO: Use a string builder?
|
|
func buildAlertSb(sb *strings.Builder, msg string, sub []string, path, avatar string, asid int) {
|
|
sb.WriteString(`{"msg":"`)
|
|
sb.WriteString(escapeTextInJson(msg))
|
|
sb.WriteString(`","sub":[`)
|
|
for i, it := range sub {
|
|
if i != 0 {
|
|
sb.WriteString(",\"")
|
|
} else {
|
|
sb.WriteString("\"")
|
|
}
|
|
sb.WriteString(escapeTextInJson(it))
|
|
sb.WriteString("\"")
|
|
}
|
|
sb.WriteString(`],"path":"`)
|
|
sb.WriteString(escapeTextInJson(path))
|
|
sb.WriteString(`","img":"`)
|
|
sb.WriteString(escapeTextInJson(avatar))
|
|
sb.WriteString(`","id":`)
|
|
sb.WriteString(strconv.Itoa(asid))
|
|
sb.WriteRune('}')
|
|
}
|
|
|
|
func BuildAlertSb(sb *strings.Builder, a *Alert, u *User /* The current user */) (err error) {
|
|
var targetUser *User
|
|
if a.Actor == nil {
|
|
a.Actor, err = Users.Get(a.ActorID)
|
|
if err != nil {
|
|
return errors.New(phrases.GetErrorPhrase("alerts_no_actor"))
|
|
}
|
|
}
|
|
|
|
/*if a.ElementType != "forum" {
|
|
targetUser, err = users.Get(a.TargetUserID)
|
|
if err != nil {
|
|
LocalErrorJS("Unable to find the target user",w,r)
|
|
return
|
|
}
|
|
}*/
|
|
if a.Event == "friend_invite" {
|
|
buildAlertSb(sb, ".new_friend_invite", []string{a.Actor.Name}, a.Actor.Link, a.Actor.Avatar, a.ASID)
|
|
return nil
|
|
}
|
|
|
|
// Not that many events for us to handle in a forum
|
|
if a.ElementType == "forum" {
|
|
if a.Event == "reply" {
|
|
topic, err := Topics.Get(a.ElementID)
|
|
if err != nil {
|
|
DebugLogf("Unable to find linked topic %d", a.ElementID)
|
|
return errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic"))
|
|
}
|
|
// Store the forum ID in the targetUser column instead of making a new one? o.O
|
|
// Add an additional column for extra information later on when we add the ability to link directly to posts. We don't need the forum data for now...
|
|
buildAlertSb(sb, ".forum_new_topic", []string{a.Actor.Name, topic.Title}, topic.Link, a.Actor.Avatar, a.ASID)
|
|
return nil
|
|
}
|
|
buildAlertSb(sb, ".forum_unknown_action", []string{a.Actor.Name}, "", a.Actor.Avatar, a.ASID)
|
|
return nil
|
|
}
|
|
|
|
var url, area string
|
|
own := false
|
|
// TODO: Avoid loading a bit of data twice
|
|
switch a.ElementType {
|
|
case "convo":
|
|
convo, err := Convos.Get(a.ElementID)
|
|
if err != nil {
|
|
DebugLogf("Unable to find linked convo %d", a.ElementID)
|
|
return errors.New(phrases.GetErrorPhrase("alerts_no_linked_convo"))
|
|
}
|
|
url = convo.Link
|
|
case "topic":
|
|
topic, err := Topics.Get(a.ElementID)
|
|
if err != nil {
|
|
DebugLogf("Unable to find linked topic %d", a.ElementID)
|
|
return errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic"))
|
|
}
|
|
url = topic.Link
|
|
area = topic.Title
|
|
own = a.TargetUserID == u.ID
|
|
case "user":
|
|
targetUser, err = Users.Get(a.ElementID)
|
|
if err != nil {
|
|
DebugLogf("Unable to find target user %d", a.ElementID)
|
|
return errors.New(phrases.GetErrorPhrase("alerts_no_target_user"))
|
|
}
|
|
area = targetUser.Name
|
|
url = targetUser.Link
|
|
own = a.TargetUserID == u.ID
|
|
case "post":
|
|
t, err := TopicByReplyID(a.ElementID)
|
|
if err != nil {
|
|
DebugLogf("Unable to find linked topic by reply ID %d", a.ElementID)
|
|
return errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic_by_reply"))
|
|
}
|
|
url = t.Link
|
|
area = t.Title
|
|
own = a.TargetUserID == u.ID
|
|
default:
|
|
return errors.New(phrases.GetErrorPhrase("alerts_invalid_elementtype"))
|
|
}
|
|
|
|
sb.WriteString(`{"msg":".`)
|
|
sb.WriteString(a.ElementType)
|
|
if own {
|
|
sb.WriteString("_own_")
|
|
} else {
|
|
sb.WriteRune('_')
|
|
}
|
|
switch a.Event {
|
|
case "create", "like", "mention", "reply":
|
|
sb.WriteString(a.Event)
|
|
}
|
|
|
|
sb.WriteString(`","sub":["`)
|
|
sb.WriteString(escapeTextInJson(a.Actor.Name))
|
|
sb.WriteString("\",\"")
|
|
sb.WriteString(escapeTextInJson(area))
|
|
sb.WriteString(`"],"path":"`)
|
|
sb.WriteString(escapeTextInJson(url))
|
|
sb.WriteString(`","img":"`)
|
|
sb.WriteString(escapeTextInJson(a.Actor.Avatar))
|
|
sb.WriteString(`","id":`)
|
|
sb.WriteString(strconv.Itoa(a.ASID))
|
|
sb.WriteRune('}')
|
|
|
|
return nil
|
|
}
|
|
|
|
//const AlertsGrowHint3 = len(`{"msg":"._","sub":["",""],"path":"","img":"","id":}`) + 3 + 2 + 2 + 2 + 2 + 1
|
|
|
|
// TODO: Create a notifier structure?
|
|
func AddActivityAndNotifyAll(a Alert) error {
|
|
id, err := Activity.Add(a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return NotifyWatchers(id)
|
|
}
|
|
|
|
// TODO: Create a notifier structure?
|
|
func AddActivityAndNotifyTarget(a Alert) error {
|
|
id, err := Activity.Add(a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ActivityMatches.Add(a.TargetUserID, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.ASID = id
|
|
|
|
// Live alerts, if the target is online and WebSockets is enabled
|
|
if EnableWebsockets {
|
|
go func() {
|
|
defer EatPanics()
|
|
_ = WsHub.pushAlert(a.TargetUserID, a)
|
|
//fmt.Println("err:",err)
|
|
}()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TODO: Create a notifier structure?
|
|
func NotifyWatchers(asid int) error {
|
|
_, err := alertStmts.notifyWatchers.Exec(asid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Alert the subscribers about this without blocking us from doing something else
|
|
if EnableWebsockets {
|
|
go func() {
|
|
defer EatPanics()
|
|
notifyWatchers(asid)
|
|
}()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func notifyWatchers(asid int) {
|
|
rows, e := alertStmts.getWatchers.Query(asid)
|
|
if e != nil && e != ErrNoRows {
|
|
LogError(e)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var uid int
|
|
var uids []int
|
|
for rows.Next() {
|
|
if e := rows.Scan(&uid); e != nil {
|
|
LogError(e)
|
|
return
|
|
}
|
|
uids = append(uids, uid)
|
|
}
|
|
if e = rows.Err(); e != nil {
|
|
LogError(e)
|
|
return
|
|
}
|
|
|
|
alert, e := Activity.Get(asid)
|
|
if e != nil && e != ErrNoRows {
|
|
LogError(e)
|
|
return
|
|
}
|
|
_ = WsHub.pushAlerts(uids, alert)
|
|
}
|
|
|
|
func DismissAlert(uid, aid int) {
|
|
_ = WsHub.PushMessage(uid, `{"event":"dismiss-alert","id":`+strconv.Itoa(aid)+`}`)
|
|
}
|