76cfcb509b
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.
234 lines
7.1 KiB
Go
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)
|
|
}
|