Commit 2f6b8396 authored by zauberstuhl's avatar zauberstuhl

Resolve "Add notification to ganggo"

parent a69ca9db
This diff is collapsed.
......@@ -53,3 +53,7 @@ required = [
[[constraint]]
name = "git.feneas.org/ganggo/gorm"
version = "1.9.3"
[[constraint]]
name = "github.com/go-gomail/gomail"
branch = "v2"
......@@ -26,32 +26,23 @@ func init() {
// redirect if logged-in
revel.InterceptFunc(redirectIfLoggedIn, revel.BEFORE, &App{})
// requires login
revel.InterceptFunc(requiresHTTPLogin, revel.BEFORE, &Setting{})
revel.InterceptFunc(requiresHTTPLogin, revel.BEFORE, &Search{})
revel.InterceptFunc(requiresLogin, revel.BEFORE, &Setting{})
revel.InterceptFunc(requiresLogin, revel.BEFORE, &Search{})
}
func redirectIfLoggedIn(c *revel.Controller) revel.Result {
result := requiresHTTPLogin(c)
result := requiresLogin(c)
if result == nil {
return c.Redirect(Stream.Index)
}
return nil
}
func requiresHTTPLogin(c *revel.Controller) revel.Result {
var session models.Session
db, err := models.OpenDatabase()
if err != nil {
c.Log.Error("Cannot open database", "error", err)
return c.RenderError(err)
}
defer db.Close()
err = db.Where("token = ?", c.Session["TOKEN"]).First(&session).Error
func requiresLogin(c *revel.Controller) revel.Result {
_, err := models.CurrentUser(c)
if err != nil {
c.Flash.Error("Please log in first")
return c.Redirect(App.Index)
c.Flash.Error(c.Message("flash.errors.login"))
return c.Redirect(User.Login)
}
return nil
}
......@@ -20,6 +20,7 @@ package controllers
import (
"github.com/revel/revel"
"git.feneas.org/ganggo/ganggo/app/models"
"strconv"
)
type Post struct {
......@@ -34,7 +35,12 @@ func (p Post) Index(guid string) revel.Result {
p.ViewArgs["currentUser"] = user
}
err = post.FindByGuidAndUser(guid, user)
postID, err := strconv.ParseUint(guid, 10, 32);
if err == nil {
err = post.FindByIDAndUser(uint(postID), user)
} else {
err = post.FindByGuidAndUser(guid, user)
}
if err != nil {
return p.NotFound(p.Message("errors.controller.post_not_found"))
}
......
......@@ -110,3 +110,13 @@ func (r Receiver) Private() revel.Result {
}
return r.Render()
}
func (r Receiver) Telegram(token string) revel.Result {
revel.Config.SetSection("ganggo")
confToken, ok := revel.Config.String("telegram.token")
if ok && confToken == token {
run.Now(jobs.TelegramReceiver{Body: r.Params.JSON})
return r.Render()
}
return r.NotFound("not found")
}
......@@ -19,7 +19,11 @@ package controllers
import (
"github.com/revel/revel"
"git.feneas.org/ganggo/ganggo/app/helpers"
"git.feneas.org/ganggo/ganggo/app/models"
"git.feneas.org/ganggo/ganggo/app/jobs/notifier"
run "github.com/revel/modules/jobs/app/jobs"
"regexp"
)
type Setting struct {
......@@ -44,3 +48,95 @@ func (s Setting) Index() revel.Result {
return s.RenderTemplate("user/settings.html")
}
func (s Setting) Update() revel.Result {
var settings models.UserSettings
var email, lang string
s.Params.Bind(&email, "email")
s.Params.Bind(&lang, "lang")
// verification tokens
var emailToken string
s.Params.Bind(&emailToken, "emailtoken")
user, err := models.CurrentUser(s.Controller)
if err != nil {
s.Log.Error("Cannot fetch current user", "error", err)
return s.RenderError(err)
}
emRegex := regexp.MustCompile(`^[\w\d\._%\+-]+@[\w\d\.-]+\.\w{2,}$`)
emailChanged := user.Settings.GetValue(models.UserSettingMailVerified) == "" ||
user.Settings.GetValue(models.UserSettingMailAddress) != email
if emailChanged && (emRegex.MatchString(email) || email == "") {
token, err := helpers.Token()
if err != nil {
s.Log.Error("Cannot generate token", "error", err)
return s.RenderError(err)
}
settings = append(settings, models.UserSettings{
models.UserSetting{
UserID: user.ID,
Key: models.UserSettingMailAddress,
Value: email,
},
models.UserSetting{
UserID: user.ID,
Key: models.UserSettingMailVerified,
Value: token,
},
}...)
if email != "" {
revel.Config.SetSection("ganggo")
host := revel.Config.StringDefault("proto", "http://") +
revel.Config.StringDefault("address", "localhost:9000")
run.Now(notifier.Notifier{Messages: []interface{}{
notifier.Mail{
To: email,
Subject: s.Message("notification.mail.verify.subject"),
Body: s.Message("notification.mail.verify.body", host, token),
Lang: user.Settings.GetValue(models.UserSettingLanguage),
},
}})
}
}
// mail verification token
if emailToken != "" &&
user.Settings.GetValue(models.UserSettingMailVerified) == emailToken {
settings = append(settings, models.UserSetting{
UserID: user.ID,
Key: models.UserSettingMailVerified,
Value: "true",
})
}
lgRegex := regexp.MustCompile(`^[\w-_]{1,}$`)
if lgRegex.MatchString(lang) || lang == "" {
settings = append(settings, models.UserSetting{
UserID: user.ID,
Key: models.UserSettingLanguage,
Value: lang,
})
}
var errors bool = false
errs := settings.Update()
for _, err := range errs {
if err != nil {
errors = true
s.Log.Error("Cannot update settings", "errors", err)
}
}
if errors || len(settings) == 0 {
s.Flash.Error(s.Message("flash.errors.settings"))
} else {
s.Flash.Success(s.Message("flash.success.settings"))
}
return s.Redirect(Setting.Index)
}
......@@ -137,6 +137,14 @@ func init() {
// register jobs running on an interval
revel.OnAppStart(func() {
run.Every(24*time.Hour, jobs.Session{})
revel.Config.SetSection("ganggo")
if revel.Config.BoolDefault("telegram.enabled", false) {
host := revel.Config.StringDefault("proto", "http://") +
revel.Config.StringDefault("address", "localhost:9000")
token := revel.Config.StringDefault("telegram.token", "")
run.Now(jobs.TelegramWebhook{Token: token, Url: host})
}
})
}
......
package notifier
//
// GangGo Application Server
// Copyright (C) 2018 Lukas Matt <lukas@zauberstuhl.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import (
"github.com/revel/revel"
"crypto/tls"
"github.com/go-gomail/gomail"
"bytes"
)
type Mail struct {
To, Subject, Body, Lang string
}
func (mail Mail) Send() {
msg := gomail.NewMessage()
revel.Config.SetSection("ganggo")
host, ok := revel.Config.String("mail.host"); if !ok {
revel.AppLog.Error("Missing mail settings!")
return
}
port, ok := revel.Config.Int("mail.port"); if !ok {
revel.AppLog.Error("Missing mail settings!")
return
}
insecure := revel.Config.BoolDefault("mail.insecure", false)
username, ok := revel.Config.String("mail.username"); if !ok {
revel.AppLog.Error("Missing mail settings!")
return
}
password, ok := revel.Config.String("mail.password"); if !ok {
revel.AppLog.Error("Missing mail settings!")
return
}
from, ok := revel.Config.String("mail.from"); if !ok {
revel.AppLog.Error("Missing mail settings!")
return
}
tmpl, err := revel.MainTemplateLoader.TemplateLang(
"notifier/mail.html", mail.Lang)
if err != nil {
revel.AppLog.Error("Mail", err.Error(), err)
return
}
var buf bytes.Buffer
err = tmpl.Render(&buf, map[string]interface{}{
revel.CurrentLocaleViewArg: mail.Lang,
"Text": mail.Body})
if err != nil {
revel.AppLog.Error("Mail", err.Error(), err)
return
}
msg.SetHeader("From", from)
msg.SetHeader("To", mail.To)
msg.SetHeader("Subject", mail.Subject)
msg.SetBody("text/html", buf.String())
dialer := gomail.NewDialer(host, port, username, password)
if insecure {
dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
err = dialer.DialAndSend(msg)
if err != nil {
revel.AppLog.Error("Mail", err.Error(), err)
return
}
}
package notifier
//
// GangGo Application Server
// Copyright (C) 2018 Lukas Matt <lukas@zauberstuhl.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import "github.com/revel/revel"
type Notifier struct {
Messages []interface{}
}
func (notifier Notifier) Run() {
revel.Config.SetSection("ganggo")
for _, message := range notifier.Messages {
switch job := message.(type) {
case Mail:
if revel.Config.BoolDefault("mail.enabled", false) {
job.Send()
}
case Telegram:
if revel.Config.BoolDefault("telegram.enabled", false) {
job.Send()
}
}
}
}
package notifier
//
// GangGo Application Server
// Copyright (C) 2018 Lukas Matt <lukas@zauberstuhl.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import (
"github.com/revel/revel"
"encoding/json"
"fmt"
"net/http"
"net/url"
)
type Telegram struct {
ID string
Text string
}
type TelegramErrorResponse struct {
Ok bool `json:"ok"`
ErrorCode int `json:"error_code"`
Description string `json:"description"`
}
func (telegram Telegram) Send() {
token, ok := revel.Config.String("telegram.token"); if !ok {
revel.AppLog.Error("Telegram", "Missing telegram settings!")
return
}
endpoint := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", token)
data := url.Values{}
data.Set("chat_id", telegram.ID)
data.Set("text", telegram.Text)
data.Set("parse_mode", "html")
resp, err := http.PostForm(endpoint, data)
if err != nil {
revel.AppLog.Error("Telegram", err.Error(), err)
return
}
if resp.StatusCode != http.StatusOK {
var telegramResp TelegramErrorResponse
err := json.NewDecoder(resp.Body).Decode(&telegramResp)
if err == nil && !telegramResp.Ok {
revel.AppLog.Error("Telegram", "telegramResp", telegramResp)
return
}
}
resp.Body.Close()
}
package jobs
//
// GangGo Application Server
// Copyright (C) 2018 Lukas Matt <lukas@zauberstuhl.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import (
"github.com/revel/revel"
"git.feneas.org/ganggo/ganggo/app/models"
"git.feneas.org/ganggo/ganggo/app/helpers"
"encoding/json"
"strconv"
)
type TelegramReceiver struct {
Body []byte
}
type TelegramMessage struct {
Message struct {
From struct {
ID int `json:"id"`
} `json:"from"`
Chat struct {
ID int `json:"id"`
} `json:"chat"`
Text string `json:"text"`
} `json:"message"`
}
func (receiver TelegramReceiver) Run() {
telegramMsg := TelegramMessage{}
err := json.Unmarshal(receiver.Body, &telegramMsg)
if err != nil {
revel.AppLog.Error("TelegramReceiver", "error", err)
return
}
var tgSetting models.UserSetting
if telegramMsg.Message.Text == "/stop" {
err = tgSetting.FindByKeyValue(models.UserSettingTelegramID,
strconv.Itoa(telegramMsg.Message.Chat.ID))
if err == nil {
token, err := helpers.Token()
if err != nil {
revel.AppLog.Error("TelegramReceiver", "error", err)
return
}
tgSetting.Key = models.UserSettingTelegramVerified
tgSetting.Value = token
err = tgSetting.Update()
if err != nil {
revel.AppLog.Error("TelegramReceiver", "error", err)
return
}
tgSetting.Key = models.UserSettingTelegramID
err = tgSetting.Delete()
if err != nil {
revel.AppLog.Error("TelegramReceiver", "error", err)
return
}
}
} else if len(telegramMsg.Message.Text) == 64 {
err = tgSetting.FindByKeyValue(
models.UserSettingTelegramID, strconv.Itoa(telegramMsg.Message.Chat.ID))
// ignore it if someone already uses the account
if err != nil {
err = tgSetting.FindByKeyValue(
models.UserSettingTelegramVerified, telegramMsg.Message.Text)
if err == nil {
var tgID models.UserSetting
tgID.UserID = tgSetting.UserID
tgID.Key = models.UserSettingTelegramID
tgID.Value = strconv.Itoa(telegramMsg.Message.Chat.ID)
tgSetting.Value = "true"
settings := models.UserSettings{tgSetting, tgID}
errList := settings.Update()
for _, err := range errList {
if err != nil {
revel.AppLog.Error("TelegramReceiver", "error", err)
return
}
}
}
}
}
}
package jobs
//
// GangGo Application Server
// Copyright (C) 2018 Lukas Matt <lukas@zauberstuhl.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import (
"github.com/revel/revel"
"encoding/json"
"fmt"
"net/http"
)
type TelegramWebhook struct {
Token, Url string
}
type TelegramErrorResponse struct {
Ok bool `json:"ok"`
ErrorCode int `json:"error_code"`
Description string `json:"description"`
}
func (webhook TelegramWebhook) Run() {
endpoint := fmt.Sprintf(
"https://api.telegram.org/bot%s/setWebhook?url=%s/receive/telegram/%s&allowed_updates=message",
webhook.Token, webhook.Url, webhook.Token)
resp, err := http.Get(endpoint)
if err != nil {
revel.AppLog.Error("TelegramWebhook", err.Error(), err)
return
}
if resp.StatusCode != http.StatusOK {
var telegramResp TelegramErrorResponse
err := json.NewDecoder(resp.Body).Decode(&telegramResp)
if err == nil && !telegramResp.Ok {
revel.AppLog.Error("TelegramWebhook", "resp", telegramResp)
return
}
}
resp.Body.Close()
}
......@@ -21,6 +21,7 @@ import (
"github.com/revel/revel"
"git.feneas.org/ganggo/gorm"
"git.feneas.org/ganggo/federation"
"git.feneas.org/ganggo/ganggo/app/helpers"
)
type SchemaMigration struct {
......@@ -46,6 +47,32 @@ func migrateSchema(db *gorm.DB) error {
//// Migrations Start ////
// related to https://git.feneas.org/ganggo/ganggo/merge_requests/76
commit = "https://git.feneas.org/ganggo/ganggo/merge_requests/76"
if _, ok := migrations[commit]; !ok {
var users Users
err := db.Find(&users).Error
if err != nil {
return err
}
for _, user := range users {
token, err := helpers.Token()
if err != nil {
return err
}
setting := UserSetting{
UserID: user.ID,
Key: UserSettingTelegramVerified,
Value: token,
}
err = setting.Update()
if err != nil {
return err
}
}
structMigrations = append(structMigrations, SchemaMigration{Commit: commit})
}
// related to https://git.feneas.org/ganggo/ganggo/merge_requests/55
commit = "https://git.feneas.org/ganggo/ganggo/merge_requests/55"
if _, ok := migrations[commit]; !ok {
......@@ -248,6 +275,12 @@ func loadSchema(db *gorm.DB) {
db.Model(oAuthToken).AddIndex("index_o_auth_token_on_user_id", "user_id")
db.Model(oAuthToken).AddIndex("index_o_auth_token_on_token", "token")
db.AutoMigrate(oAuthToken)
userSetting := &UserSetting{}
db.Model(userSetting).AddUniqueIndex("index_user_setting_on_user_id_and_key", "user_id", "key")
db.Model(userSetting).AddIndex("index_user_setting_on_key", "key")
db.Model(userSetting).AddIndex("index_user_setting_on_key_and_value", "key", "value")
db.AutoMigrate(userSetting)
}
func InitDB() {
......
package models
//
// GangGo Application Server
// Copyright (C) 2018 Lukas Matt <lukas@zauberstuhl.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import "github.com/revel/revel"
type UserSettingKey int
const (
UserSettingLanguage UserSettingKey = iota
UserSettingMailVerified UserSettingKey = iota + 10
UserSettingMailAddress
UserSettingTelegramVerified UserSettingKey = iota + 20
UserSettingTelegramID
)
type UserSetting struct {
ID uint `gorm:"primary_key"`
UserID uint
Key UserSettingKey
Value string
}
type UserSettings []UserSetting
func (settings UserSettings) GetValue(key UserSettingKey) string {
for _, setting := range settings {
if setting.Key == key {
return setting.Value
}
}
return ""
}
func (settings *UserSettings) Update() (err []error) {
for _, setting := range *settings {
err = append(err, setting.Update())
}