Compare commits
36 Commits
java_test
...
retention-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c93f9adb02 | ||
|
|
aa3d852280 | ||
|
|
6b92bace0c | ||
|
|
70c857641c | ||
|
|
d5b137ea82 | ||
|
|
8b02edb19f | ||
|
|
daf1d632e7 | ||
|
|
0e435b6bcf | ||
|
|
a2643f2cd0 | ||
|
|
abe0ef0a03 | ||
|
|
0f24ba01f5 | ||
|
|
5d5e418676 | ||
|
|
ccfd7abf83 | ||
|
|
f609cef79e | ||
|
|
ab5c02d792 | ||
|
|
20a26eac3b | ||
|
|
e7b5815039 | ||
|
|
8e313840fd | ||
|
|
df8ca468db | ||
|
|
4623e05eb5 | ||
|
|
edf6baef6e | ||
|
|
611d2684c4 | ||
|
|
8bab350624 | ||
|
|
6474ff25a7 | ||
|
|
7e5a2c4377 | ||
|
|
f27a2c68ec | ||
|
|
709a3756f0 | ||
|
|
e1a0c1c847 | ||
|
|
763217c6df | ||
|
|
66d9c100ca | ||
|
|
ba2ae29e3a | ||
|
|
a72694116a | ||
|
|
3fc24d139b | ||
|
|
50ab944579 | ||
|
|
2b28661f89 | ||
|
|
cfa02f631c |
@@ -5,7 +5,6 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
b64 "encoding/base64"
|
b64 "encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ente-io/museum/pkg/controller/collections"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -15,6 +14,8 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ente-io/museum/pkg/controller/collections"
|
||||||
|
|
||||||
"github.com/ente-io/museum/ente/base"
|
"github.com/ente-io/museum/ente/base"
|
||||||
"github.com/ente-io/museum/pkg/controller/emergency"
|
"github.com/ente-io/museum/pkg/controller/emergency"
|
||||||
"github.com/ente-io/museum/pkg/controller/file_copy"
|
"github.com/ente-io/museum/pkg/controller/file_copy"
|
||||||
@@ -983,9 +984,8 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR
|
|||||||
_ = castDb.DeleteUnclaimedCodes(context.Background(), timeUtil.MicrosecondsBeforeMinutes(60))
|
_ = castDb.DeleteUnclaimedCodes(context.Background(), timeUtil.MicrosecondsBeforeMinutes(60))
|
||||||
dataCleanupCtrl.DeleteDataCron()
|
dataCleanupCtrl.DeleteDataCron()
|
||||||
})
|
})
|
||||||
|
|
||||||
schedule(c, "@every 24h", func() {
|
schedule(c, "@every 24h", func() {
|
||||||
emailNotificationCtrl.SendStorageLimitExceededMails()
|
emailNotificationCtrl.SendStorageAlerts()
|
||||||
})
|
})
|
||||||
|
|
||||||
schedule(c, "@every 1m", func() {
|
schedule(c, "@every 1m", func() {
|
||||||
@@ -996,6 +996,10 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR
|
|||||||
pushController.ClearExpiredTokens()
|
pushController.ClearExpiredTokens()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// schedule(c, "@every 3s", func() {
|
||||||
|
// emailNotificationCtrl.SendFamilyNudgeEmail()
|
||||||
|
// })
|
||||||
|
|
||||||
scheduleAndRun(c, "@every 60m", func() {
|
scheduleAndRun(c, "@every 60m", func() {
|
||||||
emergencyCtrl.SendRecoveryReminder()
|
emergencyCtrl.SendRecoveryReminder()
|
||||||
kexCtrl.DeleteOldKeys()
|
kexCtrl.DeleteOldKeys()
|
||||||
|
|||||||
144
server/mail-templates/90_percent_storage_consumed.html
Normal file
144
server/mail-templates/90_percent_storage_consumed.html
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<meta content="text/html; charset=utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1,
|
||||||
|
minimum-scale=1" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #f0f1f3;
|
||||||
|
font-family: "Helvetica Neue", "Segoe UI", Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 27px;
|
||||||
|
margin: 0;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #f4f4f4f4;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
border-color: #ddd;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
max-width: 525px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: #0055d4;
|
||||||
|
border-radius: 3px;
|
||||||
|
text-decoration: none !important;
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 10px 30px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: #888;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gutter {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0055d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.wrap {
|
||||||
|
max-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gutter {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icons {
|
||||||
|
padding: 4px !important;
|
||||||
|
width: 24px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="gutter" style="padding: 4px"> </div>
|
||||||
|
<div class="wrap" style=" background-color: rgb(255, 255, 255); padding: 2px
|
||||||
|
30px 30px 30px; max-width: 525px; margin: 0 auto; border-radius: 5px;
|
||||||
|
font-size: 16px; ">
|
||||||
|
<p>
|
||||||
|
<p>
|
||||||
|
You have used 90% of your available storage on Ente Photos. All your photos on Ente will continue to remain safe and accessible. However, new photos will not be backed up after you get to 100%.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Please upgrade your plan so that all the new photos you take can be backed up. You can also use our referral program to get more storage at no additional cost.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For any help, please reach out to support@ente.io or reply to this email.
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div class="footer" style="text-align: center; font-size: 12px; color:
|
||||||
|
rgb(136, 136, 136)" >
|
||||||
|
<div>
|
||||||
|
<a href="https://ente.io" target="_blank" ><img
|
||||||
|
src="https://email-assets.ente.io/ente-green.png" style="width: 100px;
|
||||||
|
padding: 24px" title="Ente" alt="Ente" /></a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="https://fosstodon.org/@ente" target="_blank" ><img
|
||||||
|
src="https://email-assets.ente.io/mastodon-icon.png"
|
||||||
|
class="footer-icons" style="width: 24px; padding: 4px" title="Mastodon"
|
||||||
|
alt="Mastodon" /></a>
|
||||||
|
<a href="https://twitter.com/enteio" target="_blank" ><img
|
||||||
|
src="https://email-assets.ente.io/twitter-icon.png" class="footer-icons"
|
||||||
|
style="width: 24px; padding: 4px" title="Twitter" alt="Twitter" /></a>
|
||||||
|
<a href="https://discord.ente.io" target="_blank" ><img
|
||||||
|
src="https://email-assets.ente.io/discord-icon.png" class="footer-icons"
|
||||||
|
style="width: 24px; padding: 4px" title="Discord" alt="Discord" /></a>
|
||||||
|
<a href="https://github.com/ente-io" target="_blank" ><img
|
||||||
|
src="https://email-assets.ente.io/github-icon.png" class="footer-icons"
|
||||||
|
style="width: 24px; padding: 4px" title="GitHub" alt="GitHub" /></a>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Ente Technologies, Inc.
|
||||||
|
<br /> 1111B S Governors Ave 6032 Dover, DE 19904
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
139
server/mail-templates/nudge_for_family.html
Normal file
139
server/mail-templates/nudge_for_family.html
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<meta content="text/html; charset=utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1,
|
||||||
|
minimum-scale=1" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #f0f1f3;
|
||||||
|
font-family: "Helvetica Neue", "Segoe UI", Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 27px;
|
||||||
|
margin: 0;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #f4f4f4f4;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
border-color: #ddd;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
max-width: 525px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: #0055d4;
|
||||||
|
border-radius: 3px;
|
||||||
|
text-decoration: none !important;
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 10px 30px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: #888;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gutter {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0055d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.wrap {
|
||||||
|
max-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gutter {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icons {
|
||||||
|
padding: 4px !important;
|
||||||
|
width: 24px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="gutter" style="padding: 4px"> </div>
|
||||||
|
<div class="wrap" style=" background-color: rgb(255, 255, 255); padding: 2px
|
||||||
|
30px 30px 30px; max-width: 525px; margin: 0 auto; border-radius: 5px;
|
||||||
|
font-size: 16px; " >
|
||||||
|
<p>
|
||||||
|
Your subscription includes our family plan where you can share your subscription with upto 5 additional members at no extra cost. <br>
|
||||||
|
<br>
|
||||||
|
You can add family members to your account by going to Settings -> Manage Subscriptions -> Manage Family. For more information, you can also visit
|
||||||
|
<a href="https://help.ente.io/photos/features/family-plans">our help page</a> or reach out to support@ente.io
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div class="footer" style="text-align: center; font-size: 12px; color:
|
||||||
|
rgb(136, 136, 136)" >
|
||||||
|
<div>
|
||||||
|
<a href="https://ente.io" target="_blank" ><img
|
||||||
|
src="https://email-assets.ente.io/ente-green.png" style="width: 100px;
|
||||||
|
padding: 24px" title="Ente" alt="Ente" /></a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="https://fosstodon.org/@ente" target="_blank" ><img
|
||||||
|
src="https://email-assets.ente.io/mastodon-icon.png"
|
||||||
|
class="footer-icons" style="width: 24px; padding: 4px" title="Mastodon"
|
||||||
|
alt="Mastodon" /></a>
|
||||||
|
<a href="https://twitter.com/enteio" target="_blank" ><img
|
||||||
|
src="https://email-assets.ente.io/twitter-icon.png" class="footer-icons"
|
||||||
|
style="width: 24px; padding: 4px" title="Twitter" alt="Twitter" /></a>
|
||||||
|
<a href="https://discord.ente.io" target="_blank" ><img
|
||||||
|
src="https://email-assets.ente.io/discord-icon.png" class="footer-icons"
|
||||||
|
style="width: 24px; padding: 4px" title="Discord" alt="Discord" /></a>
|
||||||
|
<a href="https://github.com/ente-io" target="_blank" ><img
|
||||||
|
src="https://email-assets.ente.io/github-icon.png" class="footer-icons"
|
||||||
|
style="width: 24px; padding: 4px" title="GitHub" alt="GitHub" /></a>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Ente Technologies, Inc.
|
||||||
|
<br /> 1111B S Governors Ave 6032 Dover, DE 19904
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -5,9 +5,10 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ente-io/museum/pkg/controller/commonbilling"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/ente-io/museum/pkg/controller/commonbilling"
|
||||||
|
|
||||||
"github.com/ente-io/museum/pkg/repo/storagebonus"
|
"github.com/ente-io/museum/pkg/repo/storagebonus"
|
||||||
|
|
||||||
"github.com/ente-io/museum/pkg/controller/discord"
|
"github.com/ente-io/museum/pkg/controller/discord"
|
||||||
@@ -292,6 +293,7 @@ func (c *BillingController) VerifySubscription(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ente.Subscription{}, stacktrace.Propagate(err, "")
|
return ente.Subscription{}, stacktrace.Propagate(err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Replaced subscription")
|
log.Info("Replaced subscription")
|
||||||
newSubscription.ID = currentSubscription.ID
|
newSubscription.ID = currentSubscription.ID
|
||||||
if paymentProvider == ente.PlayStore &&
|
if paymentProvider == ente.PlayStore &&
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ package email
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/avct/uasurfer"
|
"github.com/avct/uasurfer"
|
||||||
"github.com/ente-io/museum/ente"
|
"github.com/ente-io/museum/ente"
|
||||||
"github.com/ente-io/museum/pkg/controller/lock"
|
"github.com/ente-io/museum/pkg/controller/lock"
|
||||||
"github.com/ente-io/museum/pkg/repo"
|
"github.com/ente-io/museum/pkg/repo"
|
||||||
"github.com/ente-io/museum/pkg/utils/email"
|
"github.com/ente-io/museum/pkg/utils/email"
|
||||||
"github.com/ente-io/museum/pkg/utils/time"
|
"github.com/ente-io/museum/pkg/utils/time"
|
||||||
|
"github.com/ente-io/stacktrace"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -21,6 +21,7 @@ const (
|
|||||||
StorageLimitExceededMailLock = "storage_limit_exceeded_mail_lock"
|
StorageLimitExceededMailLock = "storage_limit_exceeded_mail_lock"
|
||||||
StorageLimitExceededTemplateID = "storage_limit_exceeded"
|
StorageLimitExceededTemplateID = "storage_limit_exceeded"
|
||||||
StorageLimitExceededTemplate = "storage_limit_exceeded.html"
|
StorageLimitExceededTemplate = "storage_limit_exceeded.html"
|
||||||
|
StorageLimitExceededSubject = "[Alert] You have exceeded your storage limit"
|
||||||
|
|
||||||
FilesCollectedTemplate = "files_collected.html"
|
FilesCollectedTemplate = "files_collected.html"
|
||||||
FilesCollectedTemplateID = "files_collected"
|
FilesCollectedTemplateID = "files_collected"
|
||||||
@@ -33,12 +34,19 @@ const (
|
|||||||
SubscriptionCancelledTemplate = "subscription_cancelled.html"
|
SubscriptionCancelledTemplate = "subscription_cancelled.html"
|
||||||
FilesCollectedMuteDurationInMinutes = 10
|
FilesCollectedMuteDurationInMinutes = 10
|
||||||
|
|
||||||
StorageLimitExceededSubject = "[Alert] You have exceeded your storage limit"
|
ReferralSuccessfulTemplate = "successful_referral.html"
|
||||||
ReferralSuccessfulTemplate = "successful_referral.html"
|
ReferralSuccessfulSubject = "You've earned 10 GB on Ente! 🎁"
|
||||||
ReferralSuccessfulSubject = "You've earned 10 GB on Ente! 🎁"
|
|
||||||
|
StorageLimitExceedingID = "90_percent_consumed"
|
||||||
|
StorageLimitExceedingTemplate = "90_percent_storage_consumed.html"
|
||||||
|
StorageLimitExceedingSubject = "Your Ente storage is at 90% capacity"
|
||||||
|
|
||||||
LoginSuccessSubject = "New login to your Ente account"
|
LoginSuccessSubject = "New login to your Ente account"
|
||||||
LoginSuccessTemplate = "on_login.html"
|
LoginSuccessTemplate = "on_login.html"
|
||||||
|
|
||||||
|
FamilyNudgeEmailTemplate = "nudge_for_family.html"
|
||||||
|
FamilyNudgeSubject = "Share your Ente Subscription with your Family!"
|
||||||
|
FamilyNudgeTemplateID = "family_nudge"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EmailNotificationController struct {
|
type EmailNotificationController struct {
|
||||||
@@ -60,7 +68,7 @@ func (c *EmailNotificationController) OnFirstFileUpload(userID int64, userAgent
|
|||||||
}
|
}
|
||||||
err = email.SendTemplatedEmail([]string{user.Email}, "team@ente.io", "team@ente.io", FirstUploadEmailSubject, template, nil, nil)
|
err = email.SendTemplatedEmail([]string{user.Email}, "team@ente.io", "team@ente.io", FirstUploadEmailSubject, template, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error sending first upload email ", err)
|
log.Error("Error sending first upload email", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +166,7 @@ func (c *EmailNotificationController) OnSubscriptionCancelled(userID int64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *EmailNotificationController) SendStorageLimitExceededMails() {
|
func (c *EmailNotificationController) SendStorageAlerts() {
|
||||||
if c.isSendingStorageLimitExceededMails {
|
if c.isSendingStorageLimitExceededMails {
|
||||||
log.Info("Skipping sending storage limit exceeded mails as another instance is still running")
|
log.Info("Skipping sending storage limit exceeded mails as another instance is still running")
|
||||||
return
|
return
|
||||||
@@ -171,33 +179,92 @@ func (c *EmailNotificationController) SendStorageLimitExceededMails() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer c.LockController.ReleaseLock(StorageLimitExceededMailLock)
|
defer c.LockController.ReleaseLock(StorageLimitExceededMailLock)
|
||||||
users, err := c.UserRepo.GetUsersWithIndividualPlanWhoHaveExceededStorageQuota()
|
|
||||||
if err != nil {
|
// storageAlertGroups struct gets the list of both the users who have consumed
|
||||||
log.Error("Error while fetching user list", err)
|
// 90% storage and 100% of their subcriptions. Then, it ranges through
|
||||||
return
|
// the slices of the both the users and inside this for loop, users from
|
||||||
|
// both the slices are separately looped. This is done to avoid
|
||||||
|
// duplication of a lot of code if both the users were ranged inside a loop
|
||||||
|
// separately.
|
||||||
|
storageAlertGroups := []struct {
|
||||||
|
getListofSubscribers func() ([]ente.User, error)
|
||||||
|
template string
|
||||||
|
subject string
|
||||||
|
notifID string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
getListofSubscribers: func() ([]ente.User, error) {
|
||||||
|
return c.UserRepo.GetUsersWithExceedingStorage(90)
|
||||||
|
},
|
||||||
|
template: StorageLimitExceedingTemplate,
|
||||||
|
subject: StorageLimitExceedingSubject,
|
||||||
|
notifID: StorageLimitExceedingID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getListofSubscribers: func() ([]ente.User, error) {
|
||||||
|
return c.UserRepo.GetUsersWithExceedingStorage(100)
|
||||||
|
},
|
||||||
|
template: StorageLimitExceededTemplate,
|
||||||
|
subject: StorageLimitExceededSubject,
|
||||||
|
notifID: StorageLimitExceededTemplateID,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, u := range users {
|
for _, alertGroup := range storageAlertGroups {
|
||||||
lastNotificationTime, err := c.NotificationHistoryRepo.GetLastNotificationTime(u.ID, StorageLimitExceededTemplateID)
|
users, usersErr := alertGroup.getListofSubscribers()
|
||||||
logger := log.WithFields(log.Fields{
|
if usersErr != nil {
|
||||||
"user_id": u.ID,
|
log.WithError(usersErr).Error("Failed to get list of users")
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Could not fetch last notification time", err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if lastNotificationTime > 0 {
|
for _, u := range users {
|
||||||
continue
|
lastNotificationTime, err := c.NotificationHistoryRepo.GetLastNotificationTime(u.ID, alertGroup.notifID)
|
||||||
|
logger := log.WithFields(log.Fields{
|
||||||
|
"user_id": u.ID,
|
||||||
|
"group_id": alertGroup.notifID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Could not fetch last notification time", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lastNotificationTime == 0 {
|
||||||
|
logger.Info("Alerting about storage limit exceeded")
|
||||||
|
err = email.SendTemplatedEmail([]string{u.Email}, "team@ente.io", "team@ente.io", alertGroup.subject, alertGroup.template, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Info("Error notifying", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.NotificationHistoryRepo.SetLastNotificationTimeToNow(u.ID, alertGroup.notifID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.Info("Alerting about storage limit exceeded")
|
|
||||||
err = email.SendTemplatedEmail([]string{u.Email}, "team@ente.io", "team@ente.io", StorageLimitExceededSubject, StorageLimitExceededTemplate, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
logger.Info("Error notifying", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
c.NotificationHistoryRepo.SetLastNotificationTimeToNow(u.ID, StorageLimitExceededTemplateID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *EmailNotificationController) setStorageLimitExceededMailerJobStatus(isSending bool) {
|
func (c *EmailNotificationController) setStorageLimitExceededMailerJobStatus(isSending bool) {
|
||||||
c.isSendingStorageLimitExceededMails = isSending
|
c.isSendingStorageLimitExceededMails = isSending
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *EmailNotificationController) SendFamilyNudgeEmail() error {
|
||||||
|
subscribedUsers, subUsersErr := c.UserRepo.GetSubscribedUsersWithoutFamily(30)
|
||||||
|
if subUsersErr != nil {
|
||||||
|
return stacktrace.Propagate(subUsersErr, "Failed to get subscribers")
|
||||||
|
}
|
||||||
|
log.Infof("Found %d subscribers to nudge for family", len(subscribedUsers))
|
||||||
|
for _, user := range subscribedUsers {
|
||||||
|
lastNudgeSent, lastNudgeErr := c.NotificationHistoryRepo.GetLastNotificationTime(user.ID, FamilyNudgeTemplateID)
|
||||||
|
if lastNudgeErr != nil {
|
||||||
|
log.WithError(lastNudgeErr).Error("Failed to set Notification History")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lastNudgeSent == 0 {
|
||||||
|
err := email.SendTemplatedEmail([]string{user.Email}, "team@ente.io", "team@ente.io", FamilyNudgeSubject, FamilyNudgeEmailTemplate, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to send family nudge email: ", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = c.NotificationHistoryRepo.SetLastNotificationTimeToNow(user.ID, FamilyNudgeTemplateID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to set Notification History")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,7 +46,18 @@ type StripeController struct {
|
|||||||
const BufferPeriodOnPaymentFailureInDays = 7
|
const BufferPeriodOnPaymentFailureInDays = 7
|
||||||
|
|
||||||
// Return a new instance of StripeController
|
// Return a new instance of StripeController
|
||||||
func NewStripeController(plans ente.BillingPlansPerAccount, stripeClients ente.StripeClientPerAccount, billingRepo *repo.BillingRepository, fileRepo *repo.FileRepository, userRepo *repo.UserRepository, storageBonusRepo *storagebonus.Repository, discordController *discord.DiscordController, emailNotificationController *emailCtrl.EmailNotificationController, offerController *offer.OfferController, commonBillCtrl *commonbilling.Controller) *StripeController {
|
func NewStripeController(
|
||||||
|
plans ente.BillingPlansPerAccount,
|
||||||
|
stripeClients ente.StripeClientPerAccount,
|
||||||
|
billingRepo *repo.BillingRepository,
|
||||||
|
fileRepo *repo.FileRepository,
|
||||||
|
userRepo *repo.UserRepository,
|
||||||
|
storageBonusRepo *storagebonus.Repository,
|
||||||
|
discordController *discord.DiscordController,
|
||||||
|
emailNotificationController *emailCtrl.EmailNotificationController,
|
||||||
|
offerController *offer.OfferController,
|
||||||
|
commonBillCtrl *commonbilling.Controller,
|
||||||
|
) *StripeController {
|
||||||
return &StripeController{
|
return &StripeController{
|
||||||
StripeClients: stripeClients,
|
StripeClients: stripeClients,
|
||||||
BillingRepo: billingRepo,
|
BillingRepo: billingRepo,
|
||||||
@@ -249,7 +260,7 @@ func (c *StripeController) handleCheckoutSessionCompleted(event stripe.Event, co
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
|
return ente.StripeEventLog{}, stacktrace.Propagate(err, "Failed to change subscription")
|
||||||
}
|
}
|
||||||
return ente.StripeEventLog{UserID: userID, StripeSubscription: stripeSubscription, Event: event}, nil
|
return ente.StripeEventLog{UserID: userID, StripeSubscription: stripeSubscription, Event: event}, nil
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ func (repo *BillingRepository) LogStripePush(eventLog ente.StripeEventLog) error
|
|||||||
return stacktrace.Propagate(err, "")
|
return stacktrace.Propagate(err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogStripePush logs a subscription modification by an admin
|
// LogAdminTriggeredSubscriptionUpdate logs a subscription modification by an admin
|
||||||
func (repo *BillingRepository) LogAdminTriggeredSubscriptionUpdate(r ente.UpdateSubscriptionRequest) error {
|
func (repo *BillingRepository) LogAdminTriggeredSubscriptionUpdate(r ente.UpdateSubscriptionRequest) error {
|
||||||
requestJSON, _ := json.Marshal(r)
|
requestJSON, _ := json.Marshal(r)
|
||||||
_, err := repo.DB.Exec(`INSERT INTO subscription_logs(user_id, payment_provider, notification, verification_response) VALUES($1, $2, $3, '{}'::json)`,
|
_, err := repo.DB.Exec(`INSERT INTO subscription_logs(user_id, payment_provider, notification, verification_response) VALUES($1, $2, $3, '{}'::json)`,
|
||||||
|
|||||||
@@ -29,3 +29,13 @@ func (repo *NotificationHistoryRepository) SetLastNotificationTimeToNow(userID i
|
|||||||
userID, templateID, time.Microseconds())
|
userID, templateID, time.Microseconds())
|
||||||
return stacktrace.Propagate(err, "")
|
return stacktrace.Propagate(err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteLastNotificationTime deletes the last notification time for a user and template.
|
||||||
|
// This will be used in the upcoming PR for resetting storage alerts notifications.
|
||||||
|
func (repo *NotificationHistoryRepository) DeleteLastNotificationTime(userID int64, templateID string) error {
|
||||||
|
_, err := repo.DB.Exec(`DELETE FROM notification_history WHERE user_id=$1 AND template_id='$2'`, userID, templateID)
|
||||||
|
if err != nil {
|
||||||
|
return stacktrace.Propagate(err, "")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
t "time"
|
||||||
|
|
||||||
"github.com/ente-io/museum/pkg/repo/passkey"
|
"github.com/ente-io/museum/pkg/repo/passkey"
|
||||||
storageBonusRepo "github.com/ente-io/museum/pkg/repo/storagebonus"
|
storageBonusRepo "github.com/ente-io/museum/pkg/repo/storagebonus"
|
||||||
@@ -128,6 +129,39 @@ func (repo *UserRepository) GetAll(sinceTime int64, tillTime int64) ([]ente.User
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSubscribedUsersWithoutFamily returns notification candidates who are on paid plan
|
||||||
|
// but not part of any family plan.
|
||||||
|
func (repo *UserRepository) GetSubscribedUsersWithoutFamily(accountAgeInDays int64) ([]ente.User, error) {
|
||||||
|
rows, err := repo.DB.Query(`SELECT u.user_id, u.encrypted_email, u.email_decryption_nonce, u.email_hash, s.created_at
|
||||||
|
FROM subscriptions s
|
||||||
|
INNER JOIN users u ON s.user_id = u.user_id
|
||||||
|
WHERE u.creation_time <= $1 AND u.family_admin_id IS NULL
|
||||||
|
AND s.product_id != 'free'
|
||||||
|
AND u.encrypted_email IS NOT NULL`, time.MicrosecondBeforeDays(int(accountAgeInDays)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, stacktrace.Propagate(err, "")
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
subscribedUsers := make([]ente.User, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var user ente.User
|
||||||
|
var encryptedEmail, nonce []byte
|
||||||
|
var createdAt t.Time
|
||||||
|
|
||||||
|
err := rows.Scan(&user.ID, &encryptedEmail, &nonce, &user.Hash, &createdAt)
|
||||||
|
if err != nil {
|
||||||
|
return subscribedUsers, stacktrace.Propagate(err, "user scan failed")
|
||||||
|
}
|
||||||
|
email, err := crypto.Decrypt(encryptedEmail, repo.SecretEncryptionKey, nonce)
|
||||||
|
if err != nil {
|
||||||
|
return nil, stacktrace.Propagate(err, "")
|
||||||
|
}
|
||||||
|
user.Email = email
|
||||||
|
subscribedUsers = append(subscribedUsers, user)
|
||||||
|
}
|
||||||
|
return subscribedUsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserUsageWithSubData will return current storage usage & basic information about subscription for given list
|
// GetUserUsageWithSubData will return current storage usage & basic information about subscription for given list
|
||||||
// of users. It's primarily used for fetching storage utilisation for a family/group of users
|
// of users. It's primarily used for fetching storage utilisation for a family/group of users
|
||||||
func (repo *UserRepository) GetUserUsageWithSubData(ctx context.Context, userIds []int64) ([]ente.UserUsageWithSubData, error) {
|
func (repo *UserRepository) GetUserUsageWithSubData(ctx context.Context, userIds []int64) ([]ente.UserUsageWithSubData, error) {
|
||||||
@@ -280,17 +314,17 @@ func (repo *UserRepository) GetPublicKey(userID int64) (string, error) {
|
|||||||
return publicKey, stacktrace.Propagate(err, "")
|
return publicKey, stacktrace.Propagate(err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsersWithIndividualPlanWhoHaveExceededStorageQuota returns list of users who have consumed their storage quota
|
// GetUsersWithExceedingStorage returns list of users who have consumed 90% or 100% of their total storage quota
|
||||||
// and they are not part of any family plan
|
// depending on the percentageThreshold (90% or 100%) and they are not part of any family plan
|
||||||
func (repo *UserRepository) GetUsersWithIndividualPlanWhoHaveExceededStorageQuota() ([]ente.User, error) {
|
func (repo *UserRepository) GetUsersWithExceedingStorage(percentageThreshold int64) ([]ente.User, error) {
|
||||||
rows, err := repo.DB.Query(`
|
rows, err := repo.DB.Query(`
|
||||||
SELECT users.user_id, users.encrypted_email, users.email_decryption_nonce, users.email_hash, usage.storage_consumed, subscriptions.storage
|
SELECT users.user_id, users.encrypted_email, users.email_decryption_nonce, users.email_hash, usage.storage_consumed, subscriptions.storage
|
||||||
FROM users
|
FROM users
|
||||||
INNER JOIN usage
|
INNER JOIN usage
|
||||||
ON users.user_id = usage.user_id
|
ON users.user_id = usage.user_id
|
||||||
INNER JOIN subscriptions
|
INNER JOIN subscriptions
|
||||||
ON users.user_id = subscriptions.user_id AND usage.storage_consumed > subscriptions.storage AND users.encrypted_email IS NOT NULL AND users.family_admin_id IS NULL;
|
ON users.user_id = subscriptions.user_id AND usage.storage_consumed >= (subscriptions.storage * ($1 / 100.0) AND users.encrypted_email IS NOT NULL AND users.family_admin_id is NULL
|
||||||
`)
|
`, percentageThreshold)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, stacktrace.Propagate(err, "")
|
return nil, stacktrace.Propagate(err, "")
|
||||||
}
|
}
|
||||||
@@ -304,7 +338,7 @@ func (repo *UserRepository) GetUsersWithIndividualPlanWhoHaveExceededStorageQuot
|
|||||||
var user ente.User
|
var user ente.User
|
||||||
var encryptedEmail, nonce []byte
|
var encryptedEmail, nonce []byte
|
||||||
var storageConsumed, subStorage int64
|
var storageConsumed, subStorage int64
|
||||||
err := rows.Scan(&user.ID, &encryptedEmail, &nonce, &user.Hash, &storageConsumed, &subStorage)
|
err = rows.Scan(&user.ID, &encryptedEmail, &nonce, &user.Hash, &storageConsumed, &subStorage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return users, stacktrace.Propagate(err, "")
|
return users, stacktrace.Propagate(err, "")
|
||||||
}
|
}
|
||||||
@@ -312,15 +346,26 @@ func (repo *UserRepository) GetUsersWithIndividualPlanWhoHaveExceededStorageQuot
|
|||||||
if strings.EqualFold(user.Hash, fmt.Sprintf(DELETED_EMAIL_HASH_FORMAT, &user.ID)) || len(encryptedEmail) == 0 {
|
if strings.EqualFold(user.Hash, fmt.Sprintf(DELETED_EMAIL_HASH_FORMAT, &user.ID)) || len(encryptedEmail) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if refBonusStorage, ok := refBonus[user.ID]; ok {
|
refBonusStorage := int64(0)
|
||||||
addOnBonusStorage := addOnBonus[user.ID]
|
addOnBonusStorage := int64(0)
|
||||||
|
|
||||||
|
if bonus, ok := refBonus[user.ID]; ok {
|
||||||
|
refBonusStorage = bonus
|
||||||
|
addOnBonusStorage = addOnBonus[user.ID]
|
||||||
// cap usable ref bonus to the subscription storage + addOnBonus
|
// cap usable ref bonus to the subscription storage + addOnBonus
|
||||||
if refBonusStorage > (subStorage + addOnBonusStorage) {
|
if refBonusStorage > (subStorage + addOnBonusStorage) {
|
||||||
refBonusStorage = subStorage + addOnBonusStorage
|
refBonusStorage = subStorage + addOnBonusStorage
|
||||||
}
|
}
|
||||||
if (storageConsumed) <= (subStorage + refBonusStorage + addOnBonusStorage) {
|
}
|
||||||
continue
|
totalStorage := refBonusStorage + subStorage + addOnBonusStorage
|
||||||
}
|
usagePercentage := (storageConsumed * 100.0) / totalStorage
|
||||||
|
if percentageThreshold >= 100 && usagePercentage < 100.0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// when required percentage is below 100, skip email if the usage is below
|
||||||
|
// percentageThreshold or usage above 100% (which means user has used all the storage)
|
||||||
|
if percentageThreshold < 100 && (usagePercentage < percentageThreshold || usagePercentage >= 100.0) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
email, err := crypto.Decrypt(encryptedEmail, repo.SecretEncryptionKey, nonce)
|
email, err := crypto.Decrypt(encryptedEmail, repo.SecretEncryptionKey, nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -328,6 +373,7 @@ func (repo *UserRepository) GetUsersWithIndividualPlanWhoHaveExceededStorageQuot
|
|||||||
}
|
}
|
||||||
user.Email = email
|
user.Email = email
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
|
|
||||||
}
|
}
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user