gosora/common/alerts.go
Azareal 52c8be4173 Variables which are computed multiple times in templates should only be computed once now.
Enabled the Watchdog Goroutine.
Moved a couple of parser tests into their own file.
Eliminated the hard-coded URLs in TestParser()
Refactored the alert system so that a specific actor is only loaded once rather than once for each alert. This isn't a problem in practice due to the user cache, but it might be on high traffic sites.
Don't run HandleServerSync() on single server setups.
Don't load the user in GetTopicUser(), if the topic creator is the current user.
Fixed a bug in the template generator where endloop nodes were being pushed instead of endif ones.
2018-11-22 17:21:43 +10:00

248 lines
7.3 KiB
Go

/*
*
* Gosora Alerts System
* Copyright Azareal 2017 - 2019
*
*/
package common
import (
"database/sql"
"errors"
"strconv"
"strings"
"github.com/Azareal/Gosora/common/phrases"
"github.com/Azareal/Gosora/query_gen"
)
type Alert struct {
ASID int
ActorID int
TargetUserID int
Event string
ElementType string
ElementID int
Actor *User
}
type AlertStmts struct {
addActivity *sql.Stmt
notifyWatchers *sql.Stmt
notifyOne *sql.Stmt
getWatchers *sql.Stmt
getActivityEntry *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{
addActivity: acc.Insert("activity_stream").Columns("actor, targetUser, event, elementType, elementID").Fields("?,?,?,?,?").Prepare(),
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 = ?", "", ""},
),
notifyOne: acc.Insert("activity_stream_matches").Columns("watcher, asid").Fields("?,?").Prepare(),
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 = ?", "", ""),
getActivityEntry: acc.Select("activity_stream").Columns("actor, targetUser, event, elementType, elementID").Where("asid = ?").Prepare(),
}
return acc.FirstError()
})
}
// 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(alert Alert, user User /* The current user */) (out string, err error) {
var targetUser *User
if alert.Actor == nil {
alert.Actor, err = Users.Get(alert.ActorID)
if err != nil {
return "", errors.New(phrases.GetErrorPhrase("alerts_no_actor"))
}
}
/*if alert.ElementType != "forum" {
targetUser, err = users.Get(alert.TargetUserID)
if err != nil {
LocalErrorJS("Unable to find the target user",w,r)
return
}
}*/
if alert.Event == "friend_invite" {
return buildAlertString(phrases.GetTmplPhrase("alerts.new_friend_invite"), []string{alert.Actor.Name}, alert.Actor.Link, alert.Actor.Avatar, alert.ASID), nil
}
// Not that many events for us to handle in a forum
if alert.ElementType == "forum" {
if alert.Event == "reply" {
topic, err := Topics.Get(alert.ElementID)
if err != nil {
DebugLogf("Unable to find linked topic %d", alert.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(phrases.GetTmplPhrase("alerts.forum_new_topic"), []string{alert.Actor.Name, topic.Title}, topic.Link, alert.Actor.Avatar, alert.ASID), nil
}
return buildAlertString(phrases.GetTmplPhrase("alerts.forum_unknown_action"), []string{alert.Actor.Name}, "", alert.Actor.Avatar, alert.ASID), nil
}
var url, area string
var phraseName = "alerts." + alert.ElementType
switch alert.ElementType {
case "topic":
topic, err := Topics.Get(alert.ElementID)
if err != nil {
DebugLogf("Unable to find linked topic %d", alert.ElementID)
return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic"))
}
url = topic.Link
area = topic.Title
if alert.TargetUserID == user.ID {
phraseName += "_own"
}
case "user":
targetUser, err = Users.Get(alert.ElementID)
if err != nil {
DebugLogf("Unable to find target user %d", alert.ElementID)
return "", errors.New(phrases.GetErrorPhrase("alerts_no_target_user"))
}
area = targetUser.Name
url = targetUser.Link
if alert.TargetUserID == user.ID {
phraseName += "_own"
}
case "post":
topic, err := TopicByReplyID(alert.ElementID)
if err != nil {
return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic_by_reply"))
}
url = topic.Link
area = topic.Title
if alert.TargetUserID == user.ID {
phraseName += "_own"
}
default:
return "", errors.New(phrases.GetErrorPhrase("alerts_invalid_elementtype"))
}
switch alert.Event {
case "like":
phraseName += "_like"
case "mention":
phraseName += "_mention"
case "reply":
phraseName += "_reply"
}
return buildAlertString(phrases.GetTmplPhrase(phraseName), []string{alert.Actor.Name, area}, url, alert.Actor.Avatar, alert.ASID), nil
}
func buildAlertString(msg string, sub []string, path string, avatar string, asid int) string {
var substring string
for _, item := range sub {
substring += "\"" + escapeTextInJson(item) + "\","
}
if len(substring) > 0 {
substring = substring[:len(substring)-1]
}
return `{"msg":"` + escapeTextInJson(msg) + `","sub":[` + substring + `],"path":"` + escapeTextInJson(path) + `","avatar":"` + escapeTextInJson(avatar) + `","asid":"` + strconv.Itoa(asid) + `"}`
}
func AddActivityAndNotifyAll(actor int, targetUser int, event string, elementType string, elementID int) error {
res, err := alertStmts.addActivity.Exec(actor, targetUser, event, elementType, elementID)
if err != nil {
return err
}
lastID, err := res.LastInsertId()
if err != nil {
return err
}
return NotifyWatchers(lastID)
}
func AddActivityAndNotifyTarget(alert Alert) error {
res, err := alertStmts.addActivity.Exec(alert.ActorID, alert.TargetUserID, alert.Event, alert.ElementType, alert.ElementID)
if err != nil {
return err
}
lastID, err := res.LastInsertId()
if err != nil {
return err
}
err = NotifyOne(alert.TargetUserID, lastID)
if err != nil {
return err
}
alert.ASID = int(lastID)
// Live alerts, if the target is online and WebSockets is enabled
_ = WsHub.pushAlert(alert.TargetUserID, alert)
return nil
}
func NotifyOne(watcher int, asid int64) error {
_, err := alertStmts.notifyOne.Exec(watcher, asid)
return err
}
func NotifyWatchers(asid int64) 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 notifyWatchers(asid)
}
return nil
}
func notifyWatchers(asid int64) {
rows, err := alertStmts.getWatchers.Query(asid)
if err != nil && err != ErrNoRows {
LogError(err)
return
}
defer rows.Close()
var uid int
var uids []int
for rows.Next() {
err := rows.Scan(&uid)
if err != nil {
LogError(err)
return
}
uids = append(uids, uid)
}
err = rows.Err()
if err != nil {
LogError(err)
return
}
var alert = Alert{ASID: int(asid)}
err = alertStmts.getActivityEntry.QueryRow(asid).Scan(&alert.ActorID, &alert.TargetUserID, &alert.Event, &alert.ElementType, &alert.ElementID)
if err != nil && err != ErrNoRows {
LogError(err)
return
}
_ = WsHub.pushAlerts(uids, alert)
}