gosora/common/alerts.go
Azareal 76cfcb509b Moved the phrase logic to the phrases package.
Removed some initialisation boilerplate from the test code.
De-duplicated some of the common portions of the benchmarks.
Added miscinit to reduce the amount of initialisation boilerplate in the tests and to better catch initialisation errors.
Added a profile benchmark.
2018-11-01 16:43:56 +10:00

234 lines
7.1 KiB
Go

/*
*
* Gosora Alerts System
* Copyright Azareal 2017 - 2019
*
*/
package common
import (
"database/sql"
"errors"
"strconv"
"strings"
"github.com/Azareal/Gosora/query_gen"
"github.com/Azareal/Gosora/common/phrases"
)
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(asid int, event string, elementType string, actorID int, targetUserID int, elementID int, user User /* The current user */) (string, error) {
var targetUser *User
actor, err := Users.Get(actorID)
if err != nil {
return "", errors.New(phrases.GetErrorPhrase("alerts_no_actor"))
}
/*if elementType != "forum" {
targetUser, err = users.Get(targetUserID)
if err != nil {
LocalErrorJS("Unable to find the target user",w,r)
return
}
}*/
if event == "friend_invite" {
return buildAlertString(phrases.GetTmplPhrase("alerts.new_friend_invite"), []string{actor.Name}, actor.Link, actor.Avatar, asid), nil
}
// Not that many events for us to handle in a forum
if elementType == "forum" {
if event == "reply" {
topic, err := Topics.Get(elementID)
if err != nil {
DebugLogf("Unable to find linked topic %d", 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{actor.Name, topic.Title}, topic.Link, actor.Avatar, asid), nil
}
return buildAlertString(phrases.GetTmplPhrase("alerts.forum_unknown_action"), []string{actor.Name}, "", actor.Avatar, asid), nil
}
var url, area string
var phraseName = "alerts." + elementType
switch elementType {
case "topic":
topic, err := Topics.Get(elementID)
if err != nil {
DebugLogf("Unable to find linked topic %d", elementID)
return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic"))
}
url = topic.Link
area = topic.Title
if targetUserID == user.ID {
phraseName += "_own"
}
case "user":
targetUser, err = Users.Get(elementID)
if err != nil {
DebugLogf("Unable to find target user %d", elementID)
return "", errors.New(phrases.GetErrorPhrase("alerts_no_target_user"))
}
area = targetUser.Name
url = targetUser.Link
if targetUserID == user.ID {
phraseName += "_own"
}
case "post":
topic, err := TopicByReplyID(elementID)
if err != nil {
return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic_by_reply"))
}
url = topic.Link
area = topic.Title
if targetUserID == user.ID {
phraseName += "_own"
}
default:
return "", errors.New(phrases.GetErrorPhrase("alerts_invalid_elementtype"))
}
switch event {
case "like":
phraseName += "_like"
case "mention":
phraseName += "_mention"
case "reply":
phraseName += "_reply"
}
return buildAlertString(phrases.GetTmplPhrase(phraseName), []string{actor.Name, area}, url, actor.Avatar, 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(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
}
err = NotifyOne(targetUser, lastID)
if err != nil {
return err
}
// Live alerts, if the target is online and WebSockets is enabled
_ = WsHub.pushAlert(targetUser, int(lastID), event, elementType, actor, targetUser, elementID)
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 actorID, targetUserID, elementID int
var event, elementType string
err = alertStmts.getActivityEntry.QueryRow(asid).Scan(&actorID, &targetUserID, &event, &elementType, &elementID)
if err != nil && err != ErrNoRows {
LogError(err)
return
}
_ = WsHub.pushAlerts(uids, int(asid), event, elementType, actorID, targetUserID, elementID)
}