gosora/common/alerts.go

250 lines
7.3 KiB
Go

/*
*
* Gosora Alerts System
* Copyright Azareal 2017 - 2020
*
*/
package common
import (
"database/sql"
"errors"
"strconv"
"strings"
"time"
"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
CreatedAt time.Time
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, createdAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").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, createdAt").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(".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(".forum_new_topic", []string{alert.Actor.Name, topic.Title}, topic.Link, alert.Actor.Avatar, alert.ASID), nil
}
return buildAlertString(".forum_unknown_action", []string{alert.Actor.Name}, "", alert.Actor.Avatar, alert.ASID), nil
}
var url, area string
var phraseName = "." + 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(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) + `","id":` + 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, &alert.CreatedAt)
if err != nil && err != ErrNoRows {
LogError(err)
return
}
_ = WsHub.pushAlerts(uids, alert)
}