1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2025-01-08 09:20:11 +00:00

Compare commits

..

No commits in common. "e98c9b46f106dea71b8fe183952789e8ce4628bb" and "3bf722c5fe27d624313af15aa82c1ef0f1c13f06" have entirely different histories.

15 changed files with 258 additions and 567 deletions

105
api.go
View File

@ -217,7 +217,7 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
username := jfUser.Name username := jfUser.Name
email := "" email := ""
if e, ok := app.storage.emails[jfID]; ok { if e, ok := app.storage.emails[jfID]; ok {
email = e.Addr email = e.(string)
} }
for _, ombiUser := range ombiUsers { for _, ombiUser := range ombiUsers {
ombiAddr := "" ombiAddr := ""
@ -283,7 +283,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
} }
app.jf.CacheExpiry = time.Now() app.jf.CacheExpiry = time.Now()
if emailEnabled { if emailEnabled {
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true} app.storage.emails[id] = req.Email
app.storage.storeEmails() app.storage.storeEmails()
} }
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
@ -478,7 +478,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
} }
// if app.config.Section("password_resets").Key("enabled").MustBool(false) { // if app.config.Section("password_resets").Key("enabled").MustBool(false) {
if req.Email != "" { if req.Email != "" {
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true} app.storage.emails[id] = req.Email
app.storage.storeEmails() app.storage.storeEmails()
} }
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
@ -908,8 +908,8 @@ func (app *appContext) GetInvites(gc *gin.Context) {
var address string var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) { if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
app.storage.loadEmails() app.storage.loadEmails()
if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" { if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil {
address = addr.Addr address = addr.(string)
} }
} else { } else {
address = app.config.Section("ui").Key("email").String() address = app.config.Section("ui").Key("email").String()
@ -1108,14 +1108,14 @@ func (app *appContext) SetNotify(gc *gin.Context) {
} }
var address string var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) { if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
addr, ok := app.storage.emails[gc.GetString("jfId")] var ok bool
address, ok = app.storage.emails[gc.GetString("jfId")].(string)
if !ok { if !ok {
app.err.Printf("%s: Couldn't find email address. Make sure it's set", code) app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId")) app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
respond(500, "Missing user email", gc) respond(500, "Missing user email", gc)
return return
} }
address = addr.Addr
} else { } else {
address = app.config.Section("ui").Key("email").String() address = app.config.Section("ui").Key("email").String()
} }
@ -1202,7 +1202,7 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.LastActive = jfUser.LastActivityDate.Unix() user.LastActive = jfUser.LastActivityDate.Unix()
} }
if email, ok := app.storage.emails[jfUser.ID]; ok { if email, ok := app.storage.emails[jfUser.ID]; ok {
user.Email = email.Addr user.Email = email.(string)
user.NotifyThroughEmail = user.Email != "" user.NotifyThroughEmail = user.Email != ""
} }
expiry, ok := app.storage.users[jfUser.ID] expiry, ok := app.storage.users[jfUser.ID]
@ -1293,11 +1293,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
for _, jfUser := range users { for _, jfUser := range users {
id := jfUser.ID id := jfUser.ID
if address, ok := req[id]; ok { if address, ok := req[id]; ok {
contact := true app.storage.emails[id] = address
if oldAddr, ok := app.storage.emails[id]; ok {
contact = oldAddr.Contact
}
app.storage.emails[id] = EmailAddress{Addr: address, Contact: contact}
if ombiEnabled { if ombiEnabled {
ombiUser, code, err := app.getOmbiUser(id) ombiUser, code, err := app.getOmbiUser(id)
if code == 200 && err == nil { if code == 200 && err == nil {
@ -2048,7 +2044,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
// @Summary Sets whether to notify a user through telegram or not. // @Summary Sets whether to notify a user through telegram or not.
// @Produce json // @Produce json
// @Param SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram." // @Param telegramNotifyDTO body telegramNotifyDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
// @Success 400 {object} boolResponse // @Success 400 {object} boolResponse
// @Success 500 {object} boolResponse // @Success 500 {object} boolResponse
@ -2056,7 +2052,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
// @Security Bearer // @Security Bearer
// @tags Other // @tags Other
func (app *appContext) SetContactMethods(gc *gin.Context) { func (app *appContext) SetContactMethods(gc *gin.Context) {
var req SetContactMethodsDTO var req telegramNotifyDTO
gc.BindJSON(&req) gc.BindJSON(&req)
if req.ID == "" { if req.ID == "" {
respondBool(400, false, gc) respondBool(400, false, gc)
@ -2072,9 +2068,9 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
} }
msg := "" msg := ""
if !req.Telegram { if !req.Telegram {
msg = " not" msg = "not"
} }
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg) app.debug.Printf("Telegram: User \"%s\" will %s be notified through Telegram.", tgUser.Username, msg)
} }
if dcUser, ok := app.storage.discord[req.ID]; ok { if dcUser, ok := app.storage.discord[req.ID]; ok {
dcUser.Contact = req.Discord dcUser.Contact = req.Discord
@ -2086,23 +2082,9 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
} }
msg := "" msg := ""
if !req.Discord { if !req.Discord {
msg = " not" msg = "not"
} }
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg) app.debug.Printf("Discord: User \"%s\" will %s be notified through Discord.", dcUser.Username, msg)
}
if email, ok := app.storage.emails[req.ID]; ok {
email.Contact = req.Email
app.storage.emails[req.ID] = email
if err := app.storage.storeEmails(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Failed to store emails: %v", err)
return
}
msg := ""
if !req.Email {
msg = " not"
}
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
} }
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -2164,7 +2146,7 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code. // @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
// @Produce json // @Produce json
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
// @Failure 401 {object} boolResponse // @Success 401 {object} boolResponse
// @Param pin path string true "PIN code to check" // @Param pin path string true "PIN code to check"
// @Param invCode path string true "invite Code" // @Param invCode path string true "invite Code"
// @Router /invite/{invCode}/discord/verified/{pin} [get] // @Router /invite/{invCode}/discord/verified/{pin} [get]
@ -2180,61 +2162,6 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
respondBool(200, ok, gc) respondBool(200, ok, gc)
} }
// @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional).
// @Produce json
// @Success 200 {object} DiscordUsersDTO
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param username path string true "username to search."
// @Router /users/discord/{username} [get]
// @tags Other
func (app *appContext) DiscordGetUsers(gc *gin.Context) {
name := gc.Param("username")
if name == "" {
respondBool(400, false, gc)
return
}
users := app.discord.GetUsers(name)
resp := DiscordUsersDTO{Users: make([]DiscordUserDTO, len(users))}
for i, u := range users {
resp.Users[i] = DiscordUserDTO{
Name: u.User.Username + "#" + u.User.Discriminator,
ID: u.User.ID,
AvatarURL: u.User.AvatarURL("32"),
}
}
gc.JSON(200, resp)
}
// @Summary Links a Discord account to a Jellyfin account via user IDs. Notifications are turned on by default.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID."
// @Router /users/discord [post]
// @tags Other
func (app *appContext) DiscordConnect(gc *gin.Context) {
var req DiscordConnectUserDTO
gc.BindJSON(&req)
if req.JellyfinID == "" || req.DiscordID == "" {
respondBool(400, false, gc)
return
}
user, ok := app.discord.NewUser(req.DiscordID)
if !ok {
respondBool(500, false, gc)
return
}
app.storage.discord[req.JellyfinID] = user
if err := app.storage.storeDiscordUsers(); err != nil {
app.err.Printf("Failed to store Discord users: %v", err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Restarts the program. No response means success. // @Summary Restarts the program. No response means success.
// @Router /restart [post] // @Router /restart [post]
// @Security Bearer // @Security Bearer

View File

@ -171,28 +171,3 @@ func (app *appContext) migrateEmailConfig() {
} }
app.loadConfig() app.loadConfig()
} }
func (app *appContext) migrateEmailStorage() error {
var emails map[string]interface{}
err := loadJSON(app.storage.emails_path, &emails)
if err != nil {
return err
}
newEmails := map[string]EmailAddress{}
for jfID, addr := range emails {
newEmails[jfID] = EmailAddress{
Addr: addr.(string),
Contact: true,
}
}
err = storeJSON(app.storage.emails_path+".bak", emails)
if err != nil {
return err
}
err = storeJSON(app.storage.emails_path, newEmails)
if err != nil {
return err
}
app.info.Println("Migrated to new email format. A backup has also been made.")
return nil
}

View File

@ -30,12 +30,12 @@
} }
} }
@media screen and (max-width: 1000px) { @media screen and (max-width: 750px) {
:root { :root {
font-size: 0.9rem; font-size: 0.9rem;
} }
.table-responsive table { .table-responsive table {
min-width: 800px; min-width: 660px;
} }
} }
@ -130,10 +130,6 @@ div.card:contains(section.banner.footer) {
text-align: center; text-align: center;
} }
.w-100 {
width: 100%;
}
.inline-block { .inline-block {
display: inline-block; display: inline-block;
} }
@ -428,7 +424,6 @@ p.top {
.table-responsive { .table-responsive {
overflow-x: auto; overflow-x: auto;
font-size: 0.9rem;
} }
#notification-box { #notification-box {
@ -443,10 +438,6 @@ p.top {
margin-bottom: -0.5rem; margin-bottom: -0.5rem;
} }
.dropdown-display.lg {
white-space: nowrap;
}
pre { pre {
white-space: pre-wrap; /* css-3 */ white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
@ -492,27 +483,3 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
max-width: 15rem; max-width: 15rem;
min-width: 10rem; min-width: 10rem;
} }
td.img-circle {
width: 32px;
height: 32px;
}
span.shield.img-circle {
padding: 0.2rem;
}
img.img-circle {
border-radius: 50%;
vertical-align: middle;
}
.table td.sm {
padding-top: 0.1rem;
padding-bottom: 0.1rem;
}
.table-inline {
display: flex !important;
align-items: center;
}

View File

@ -41,7 +41,6 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
for _, user := range app.storage.discord { for _, user := range app.storage.discord {
dd.users[user.ID] = user dd.users[user.ID] = user
} }
return dd, nil return dd, nil
} }
@ -71,7 +70,7 @@ func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string)
func (d *DiscordDaemon) run() { func (d *DiscordDaemon) run() {
d.bot.AddHandler(d.messageHandler) d.bot.AddHandler(d.messageHandler)
d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages
if err := d.bot.Open(); err != nil { if err := d.bot.Open(); err != nil {
d.app.err.Printf("Discord: Failed to start daemon: %v", err) d.app.err.Printf("Discord: Failed to start daemon: %v", err)
return return
@ -92,53 +91,6 @@ func (d *DiscordDaemon) run() {
return return
} }
// Returns the user(s) roughly corresponding to the username (if they are in the guild).
// if no discriminator (#xxxx) is given in the username and there are multiple corresponding users, a list of all matching users is returned.
func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
members, err := d.bot.GuildMembers(
d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID,
"",
1000,
)
if err != nil {
d.app.err.Printf("Discord: Failed to get members: %v", err)
return nil
}
hasDiscriminator := strings.Contains(username, "#")
var users []*dg.Member
for _, member := range members {
if !hasDiscriminator {
userSplit := strings.Split(member.User.Username, "#")
if strings.Contains(userSplit[0], username) {
users = append(users, member)
}
} else if strings.Contains(member.User.Username, username) {
return nil
}
}
return users
}
func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) {
u, err := d.bot.User(ID)
if err != nil {
d.app.err.Printf("Discord: Failed to get user: %v", err)
return
}
user.ID = ID
user.Username = u.Username
user.Contact = true
user.Discriminator = u.Discriminator
channel, err := d.bot.UserChannelCreate(ID)
if err != nil {
d.app.err.Printf("Discord: Failed to create DM channel: %v", err)
return
}
user.ChannelID = channel.ID
ok = true
return
}
func (d *DiscordDaemon) Shutdown() { func (d *DiscordDaemon) Shutdown() {
d.Stopped = true d.Stopped = true
d.ShutdownChannel <- "Down" d.ShutdownChannel <- "Down"
@ -283,7 +235,16 @@ func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
msg := "" msg := ""
var embeds []*dg.MessageEmbed var embeds []*dg.MessageEmbed
if message.Markdown != "" { if message.Markdown != "" {
msg, embeds = StripAltText(message.Markdown, true) var links []Link
msg, links = StripAltText(message.Markdown, true)
embeds = make([]*dg.MessageEmbed, len(links))
for i := range links {
embeds[i] = &dg.MessageEmbed{
URL: links[i].URL,
Title: links[i].Alt,
Type: dg.EmbedTypeLink,
}
}
} else { } else {
msg = message.Text msg = message.Text
} }

View File

@ -817,8 +817,8 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
return err return err
} }
} }
if address, ok := app.storage.emails[id]; ok && address.Contact && emailEnabled { if address, ok := app.storage.emails[id]; ok {
err = app.email.send(email, address.Addr) err = app.email.send(email, address.(string))
if err != nil { if err != nil {
return err return err
} }
@ -838,7 +838,7 @@ func (app *appContext) getAddressOrName(jfID string) string {
return "@" + tgChat.Username return "@" + tgChat.Username
} }
if addr, ok := app.storage.emails[jfID]; ok { if addr, ok := app.storage.emails[jfID]; ok {
return addr.Addr return addr.(string)
} }
return "" return ""
} }

View File

@ -328,18 +328,6 @@
</div> </div>
</div> </div>
{{ end }} {{ end }}
{{ if .discord_enabled }}
<div id="modal-discord" class="modal">
<div class="modal-content card">
<span class="heading mb-1">{{ .strings.linkDiscord }}<span class="modal-close">&times;</span></span>
<p class="content mb-1">{{ .strings.searchDiscordUser }}</p>
<div class="row">
<input type="search" class="col sm field ~neutral !normal input" id="discord-search" placeholder="user#1234">
</div>
<table class="table"><tbody id="discord-list"></tbody></table>
</div>
</div>
{{ end }}
<div id="notification-box"></div> <div id="notification-box"></div>
<span class="dropdown" tabindex="0" id="lang-dropdown"> <span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button"> <span class="button ~urge dropdown-button">

View File

@ -20,7 +20,6 @@
"create": "Create", "create": "Create",
"apply": "Apply", "apply": "Apply",
"delete": "Delete", "delete": "Delete",
"add": "Add",
"name": "Name", "name": "Name",
"date": "Date", "date": "Date",
"enabled": "Enabled", "enabled": "Enabled",
@ -95,8 +94,7 @@
"notifyEvent": "Notify on:", "notifyEvent": "Notify on:",
"notifyInviteExpiry": "On expiry", "notifyInviteExpiry": "On expiry",
"notifyUserCreation": "On user creation", "notifyUserCreation": "On user creation",
"sendPIN": "Ask the user to send the PIN below to the bot.", "sendPIN": "Ask the user to send the PIN below to the bot."
"searchDiscordUser": "Start typing the Discord username to link it."
}, },
"notifications": { "notifications": {
"changedEmailAddress": "Changed email address of {n}.", "changedEmailAddress": "Changed email address of {n}.",
@ -109,7 +107,6 @@
"updateApplied": "Update applied, please restart.", "updateApplied": "Update applied, please restart.",
"updateAppliedRefresh": "Update applied, please refresh.", "updateAppliedRefresh": "Update applied, please refresh.",
"telegramVerified": "Telegram account verified.", "telegramVerified": "Telegram account verified.",
"accountConnected": "Account connected.",
"errorConnection": "Couldn't connect to jfa-go.", "errorConnection": "Couldn't connect to jfa-go.",
"error401Unauthorized": "Unauthorized. Try refreshing the page.", "error401Unauthorized": "Unauthorized. Try refreshing the page.",
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.", "errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",

143
main.go
View File

@ -16,6 +16,7 @@ import (
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "time"
@ -320,10 +321,6 @@ func start(asDaemon, firstCall bool) {
app.storage.emails_path = app.config.Section("files").Key("emails").String() app.storage.emails_path = app.config.Section("files").Key("emails").String()
if err := app.storage.loadEmails(); err != nil { if err := app.storage.loadEmails(); err != nil {
app.err.Printf("Failed to load Emails: %v", err) app.err.Printf("Failed to load Emails: %v", err)
err := app.migrateEmailStorage()
if err != nil {
app.err.Printf("Failed to migrate Email storage: %v", err)
}
} }
app.storage.policy_path = app.config.Section("files").Key("user_template").String() app.storage.policy_path = app.config.Section("files").Key("user_template").String()
if err := app.storage.loadPolicy(); err != nil { if err := app.storage.loadPolicy(); err != nil {
@ -437,76 +434,76 @@ func start(asDaemon, firstCall bool) {
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s (%d): %v", server, status, err) app.err.Fatalf("Failed to authenticate with Jellyfin @ %s (%d): %v", server, status, err)
} }
app.info.Printf("Authenticated with %s", server) app.info.Printf("Authenticated with %s", server)
// /* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs. /* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs.
// This checks if the version is equal or higher. */ This checks if the version is equal or higher. */
// checkVersion := func(version string) int { checkVersion := func(version string) int {
// numberStrings := strings.Split(version, ".") numberStrings := strings.Split(version, ".")
// n := 0 n := 0
// for _, s := range numberStrings { for _, s := range numberStrings {
// num, err := strconv.Atoi(s) num, err := strconv.Atoi(s)
// if err == nil { if err == nil {
// n += num n += num
// } }
// } }
// return n return n
// } }
// if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") { if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") {
// // Get users to check if server uses hyphenated userIDs // Get users to check if server uses hyphenated userIDs
// app.jf.GetUsers(false) app.jf.GetUsers(false)
// noHyphens := true noHyphens := true
// for id := range app.storage.emails { for id := range app.storage.emails {
// if strings.Contains(id, "-") { if strings.Contains(id, "-") {
// noHyphens = false noHyphens = false
// break break
// } }
// } }
// if noHyphens == app.jf.Hyphens { if noHyphens == app.jf.Hyphens {
// var newEmails map[string]interface{} var newEmails map[string]interface{}
// var newUsers map[string]time.Time var newUsers map[string]time.Time
// var status, status2 int var status, status2 int
// var err, err2 error var err, err2 error
// if app.jf.Hyphens { if app.jf.Hyphens {
// app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match.")) app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match."))
// time.Sleep(time.Second * time.Duration(3)) time.Sleep(time.Second * time.Duration(3))
// newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails) newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails)
// newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users) newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users)
// } else { } else {
// app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified.")) app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
// time.Sleep(time.Second * time.Duration(3)) time.Sleep(time.Second * time.Duration(3))
// newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails) newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails)
// newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users) newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users)
// } }
// if status != 200 || err != nil { if status != 200 || err != nil {
// app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
// app.err.Fatalf("Couldn't upgrade emails.json") app.err.Fatalf("Couldn't upgrade emails.json")
// } }
// if status2 != 200 || err2 != nil { if status2 != 200 || err2 != nil {
// app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
// app.err.Fatalf("Couldn't upgrade users.json") app.err.Fatalf("Couldn't upgrade users.json")
// } }
// emailBakFile := app.storage.emails_path + ".bak" emailBakFile := app.storage.emails_path + ".bak"
// usersBakFile := app.storage.users_path + ".bak" usersBakFile := app.storage.users_path + ".bak"
// err = storeJSON(emailBakFile, app.storage.emails) err = storeJSON(emailBakFile, app.storage.emails)
// err2 = storeJSON(usersBakFile, app.storage.users) err2 = storeJSON(usersBakFile, app.storage.users)
// if err != nil { if err != nil {
// app.err.Fatalf("couldn't store emails.json backup: %v", err) app.err.Fatalf("couldn't store emails.json backup: %v", err)
// } }
// if err2 != nil { if err2 != nil {
// app.err.Fatalf("couldn't store users.json backup: %v", err) app.err.Fatalf("couldn't store users.json backup: %v", err)
// } }
// app.storage.emails = newEmails app.storage.emails = newEmails
// app.storage.users = newUsers app.storage.users = newUsers
// err = app.storage.storeEmails() err = app.storage.storeEmails()
// err2 = app.storage.storeUsers() err2 = app.storage.storeUsers()
// if err != nil { if err != nil {
// app.err.Fatalf("couldn't store emails.json: %v", err) app.err.Fatalf("couldn't store emails.json: %v", err)
// } }
// if err2 != nil { if err2 != nil {
// app.err.Fatalf("couldn't store users.json: %v", err) app.err.Fatalf("couldn't store users.json: %v", err)
// } }
// } }
// } }
// Auth (manual user/pass or jellyfin) // Auth (manual user/pass or jellyfin)
app.jellyfinLogin = true app.jellyfinLogin = true

View File

@ -255,24 +255,9 @@ type telegramSetDTO struct {
ID string `json:"id"` // Jellyfin ID of user. ID string `json:"id"` // Jellyfin ID of user.
} }
type SetContactMethodsDTO struct { type telegramNotifyDTO struct {
ID string `json:"id"` ID string `json:"id"`
Email bool `json:"email"` Email bool `json:"email"`
Discord bool `json:"discord"` Discord bool `json:"discord"`
Telegram bool `json:"telegram"` Telegram bool `json:"telegram"`
} }
type DiscordUserDTO struct {
Name string `json:"name"`
AvatarURL string `json:"avatar_url"`
ID string `json:"id"`
}
type DiscordUsersDTO struct {
Users []DiscordUserDTO `json:"users"`
}
type DiscordConnectUserDTO struct {
JellyfinID string `json:"jf_id"`
DiscordID string `json:"discord_id"`
}

View File

@ -161,16 +161,12 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/config", app.GetConfig) api.GET(p+"/config", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig) api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart) api.POST(p+"/restart", app.restart)
if telegramEnabled || discordEnabled { if telegramEnabled {
api.GET(p+"/telegram/pin", app.TelegramGetPin) api.GET(p+"/telegram/pin", app.TelegramGetPin)
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified) api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
api.POST(p+"/users/telegram", app.TelegramAddUser) api.POST(p+"/users/telegram", app.TelegramAddUser)
api.POST(p+"/users/contact", app.SetContactMethods) api.POST(p+"/users/contact", app.SetContactMethods)
} }
if discordEnabled {
api.GET(p+"/users/discord/:username", app.DiscordGetUsers)
api.POST(p+"/users/discord", app.DiscordConnect)
}
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
api.GET(p+"/ombi/users", app.OmbiUsers) api.GET(p+"/ombi/users", app.OmbiUsers)
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults) api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)

View File

@ -21,8 +21,7 @@ type Storage struct {
invites Invites invites Invites
profiles map[string]Profile profiles map[string]Profile
defaultProfile string defaultProfile string
displayprefs, ombi_template map[string]interface{} emails, displayprefs, ombi_template map[string]interface{}
emails map[string]EmailAddress
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users. telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users. discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
customEmails customEmails customEmails customEmails
@ -48,11 +47,6 @@ type DiscordUser struct {
Contact bool Contact bool
} }
type EmailAddress struct {
Addr string
Contact bool
}
type customEmails struct { type customEmails struct {
UserCreated customEmail `json:"userCreated"` UserCreated customEmail `json:"userCreated"`
InviteExpiry customEmail `json:"inviteExpiry"` InviteExpiry customEmail `json:"inviteExpiry"`
@ -908,85 +902,85 @@ func storeJSON(path string, obj interface{}) error {
return err return err
} }
// // One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage. // One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage.
//
// func hyphenate(userID string) string { func hyphenate(userID string) string {
// if userID[8] == '-' { if userID[8] == '-' {
// return userID return userID
// } }
// return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:] return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
// } }
//
// func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) { func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
// jfUsers, status, err := app.jf.GetUsers(false) jfUsers, status, err := app.jf.GetUsers(false)
// if status != 200 || err != nil { if status != 200 || err != nil {
// return nil, status, err return nil, status, err
// } }
// newEmails := map[string]interface{}{} newEmails := map[string]interface{}{}
// for _, user := range jfUsers { for _, user := range jfUsers {
// unHyphenated := user.ID unHyphenated := user.ID
// hyphenated := hyphenate(unHyphenated) hyphenated := hyphenate(unHyphenated)
// val, ok := old[hyphenated] val, ok := old[hyphenated]
// if ok { if ok {
// newEmails[unHyphenated] = val newEmails[unHyphenated] = val
// } }
// } }
// return newEmails, status, err return newEmails, status, err
// } }
//
// func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) { func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
// jfUsers, status, err := app.jf.GetUsers(false) jfUsers, status, err := app.jf.GetUsers(false)
// if status != 200 || err != nil { if status != 200 || err != nil {
// return nil, status, err return nil, status, err
// } }
// newEmails := map[string]interface{}{} newEmails := map[string]interface{}{}
// for _, user := range jfUsers { for _, user := range jfUsers {
// unstripped := user.ID unstripped := user.ID
// stripped := strings.ReplaceAll(unstripped, "-", "") stripped := strings.ReplaceAll(unstripped, "-", "")
// val, ok := old[stripped] val, ok := old[stripped]
// if ok { if ok {
// newEmails[unstripped] = val newEmails[unstripped] = val
// } }
// } }
// return newEmails, status, err return newEmails, status, err
// } }
//
// func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) { func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
// return app.hyphenateStorage(old) return app.hyphenateStorage(old)
// } }
//
// func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) { func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
// return app.deHyphenateStorage(old) return app.deHyphenateStorage(old)
// } }
//
// func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) { func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
// asInterface := map[string]interface{}{} asInterface := map[string]interface{}{}
// for k, v := range old { for k, v := range old {
// asInterface[k] = v asInterface[k] = v
// } }
// fixed, status, err := app.hyphenateStorage(asInterface) fixed, status, err := app.hyphenateStorage(asInterface)
// if err != nil { if err != nil {
// return nil, status, err return nil, status, err
// } }
// out := map[string]time.Time{} out := map[string]time.Time{}
// for k, v := range fixed { for k, v := range fixed {
// out[k] = v.(time.Time) out[k] = v.(time.Time)
// } }
// return out, status, err return out, status, err
// } }
//
// func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) { func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
// asInterface := map[string]interface{}{} asInterface := map[string]interface{}{}
// for k, v := range old { for k, v := range old {
// asInterface[k] = v asInterface[k] = v
// } }
// fixed, status, err := app.deHyphenateStorage(asInterface) fixed, status, err := app.deHyphenateStorage(asInterface)
// if err != nil { if err != nil {
// return nil, status, err return nil, status, err
// } }
// out := map[string]time.Time{} out := map[string]time.Time{}
// for k, v := range fixed { for k, v := range fixed {
// out[k] = v.(time.Time) out[k] = v.(time.Time)
// } }
// return out, status, err return out, status, err
// } }

View File

@ -3,7 +3,6 @@ package main
import ( import (
"strings" "strings"
dg "github.com/bwmarrin/discordgo"
stripmd "github.com/writeas/go-strip-markdown" stripmd "github.com/writeas/go-strip-markdown"
) )
@ -14,32 +13,22 @@ type Link struct {
// StripAltText removes Markdown alt text from links and images and replaces them with just the URL. // StripAltText removes Markdown alt text from links and images and replaces them with just the URL.
// Currently uses the deepest alt text when links/images are nested. // Currently uses the deepest alt text when links/images are nested.
// If links = true, links are completely removed, and a list of URLs and their alt text is also returned. // If links = true, links are completely removed, and a list of URLs and their alt text is also returned.
func StripAltText(md string, links bool) (string, []*dg.MessageEmbed) { func StripAltText(md string, links bool) (string, []Link) {
altTextStart := -1 // Start of alt text (between '[' & ']') altTextStart := -1 // Start of alt text (between '[' & ']')
URLStart := -1 // Start of url (between '(' & ')') URLStart := -1 // Start of url (between '(' & ')')
URLEnd := -1 URLEnd := -1
previousURLEnd := -2 previousURLEnd := -2
out := "" out := ""
embeds := []*dg.MessageEmbed{} embeds := []Link{}
for i := range md { for i := range md {
if altTextStart != -1 && URLStart != -1 && md[i] == ')' { if altTextStart != -1 && URLStart != -1 && md[i] == ')' {
URLEnd = i - 1 URLEnd = i - 1
out += md[previousURLEnd+2 : altTextStart-1] out += md[previousURLEnd+2 : altTextStart-1]
if links { if links {
embed := &dg.MessageEmbed{ embeds = append(embeds, Link{
Type: dg.EmbedTypeLink, URL: md[URLStart : URLEnd+1],
Title: md[altTextStart : URLStart-2], Alt: md[altTextStart : URLStart-2],
} })
if md[altTextStart-1] == '!' {
embed.Title = md[altTextStart+1 : URLStart-2]
embed.Type = dg.EmbedTypeImage
embed.Image = &dg.MessageEmbedImage{
URL: md[URLStart : URLEnd+1],
}
} else {
embed.URL = md[URLStart : URLEnd+1]
}
embeds = append(embeds, embed)
} else { } else {
out += md[URLStart : URLEnd+1] out += md[URLStart : URLEnd+1]
} }

View File

@ -66,10 +66,6 @@ window.availableProfiles = window.availableProfiles || [];
if (window.telegramEnabled) { if (window.telegramEnabled) {
window.modals.telegram = new Modal(document.getElementById("modal-telegram")); window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
} }
if (window.discordEnabled) {
window.modals.discord = new Modal(document.getElementById("modal-discord"));
}
})(); })();
var inviteCreator = new createInvite(); var inviteCreator = new createInvite();

View File

@ -24,12 +24,6 @@ interface getPinResponse {
username: string; username: string;
} }
interface DiscordUser {
name: string;
avatar_url: string;
id: string;
}
class user implements User { class user implements User {
private _row: HTMLTableRowElement; private _row: HTMLTableRowElement;
private _check: HTMLInputElement; private _check: HTMLInputElement;
@ -102,9 +96,7 @@ class user implements User {
this._notifyEmail = s; this._notifyEmail = s;
if (window.telegramEnabled && this._telegramUsername != "") { if (window.telegramEnabled && this._telegramUsername != "") {
const email = this._telegram.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement; const email = this._telegram.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
if (email) { email.checked = s;
email.checked = s;
}
} }
if (window.discordEnabled && this._discordUsername != "") { if (window.discordEnabled && this._discordUsername != "") {
const email = this._discord.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement; const email = this._discord.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
@ -122,50 +114,52 @@ class user implements User {
} else { } else {
let innerHTML = ` let innerHTML = `
<a href="https://t.me/${u}" target="_blank">@${u}</a> <a href="https://t.me/${u}" target="_blank">@${u}</a>
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
<div class="dropdown manual">
<div class="dropdown-display">
<div class="card ~neutral !low">
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
<label class="switch pb-1 mt-half">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
<span>Email</span>
</label>
<label class="switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
<span>Telegram</span>
</label>
`; `;
if (!window.discordEnabled || this._discordUsername == "") { if (window.discordEnabled && this._discordUsername != "") {
innerHTML += ` innerHTML += `
<div class="table-inline"> <label class="switch pb-1">
<i class="icon ri-settings-2-line ml-half dropdown-button"></i> <input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord">
<div class="dropdown manual"> <span>Discord</span>
<div class="dropdown-display lg"> </label>
<div class="card ~neutral !low">
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
<label class="row switch pb-1 mt-half">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
<span>Email</span>
</label>
<label class="row switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
<span>Telegram</span>
</label>
</div>
</div>
</div>
</div>
`; `;
} }
this._telegram.innerHTML = innerHTML; innerHTML += `
if (!window.discordEnabled || this._discordUsername == "") { </div>
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username. </div>
const button = this._telegram.querySelector("i"); </div>
const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement; `;
const checks = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>; this._discord.innerHTML = innerHTML;
for (let i = 0; i < checks.length; i++) { // Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
checks[i].onclick = () => this._setNotifyMethod("telegram"); const button = this._telegram.querySelector("i");
} const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
const checks = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
button.onclick = () => { for (let i = 0; i < checks.length; i++) {
dropdown.classList.add("selected"); checks[i].onclick = () => this._setNotifyMethod("telegram");
document.addEventListener("click", outerClickListener);
};
const outerClickListener = (event: Event) => {
if (!(event.target instanceof HTMLElement && (this._telegram.contains(event.target) || button.contains(event.target)))) {
dropdown.classList.remove("selected");
document.removeEventListener("click", outerClickListener);
}
};
} }
button.onclick = () => {
dropdown.classList.add("selected");
document.addEventListener("click", outerClickListener);
};
const outerClickListener = (event: Event) => {
if (!(event.target instanceof HTMLElement && (this._telegram.contains(event.target) || button.contains(event.target)))) {
dropdown.classList.remove("selected");
document.removeEventListener("click", outerClickListener);
}
};
} }
} }
@ -174,9 +168,7 @@ class user implements User {
if (!window.telegramEnabled || !this._telegramUsername) return; if (!window.telegramEnabled || !this._telegramUsername) return;
this._notifyTelegram = s; this._notifyTelegram = s;
const telegram = this._telegram.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement; const telegram = this._telegram.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
if (telegram) { telegram.checked = s;
telegram.checked = s;
}
if (window.discordEnabled && this._discordUsername != "") { if (window.discordEnabled && this._discordUsername != "") {
const telegram = this._discord.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement; const telegram = this._discord.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
telegram.checked = s; telegram.checked = s;
@ -226,35 +218,33 @@ class user implements User {
this._discordUsername = u; this._discordUsername = u;
if (u == "") { if (u == "") {
this._discord.innerHTML = `<span class="chip btn !low">Add</span>`; this._discord.innerHTML = `<span class="chip btn !low">Add</span>`;
(this._discord.querySelector("span") as HTMLSpanElement).onclick = this._addDiscord; // (this._discord.querySelector("span") as HTMLSpanElement).onclick = this._addDiscord;
} else { } else {
let innerHTML = ` let innerHTML = `
<div class="table-inline"> <a href="https://discord.com/users/${this._discordID}" class="discord-link" target="_blank">@${u}</a>
<a href="https://discord.com/users/${this._discordID}" class="discord-link" target="_blank">${u}</a> <i class="icon ri-settings-2-line ml-half dropdown-button"></i>
<i class="icon ri-settings-2-line ml-half dropdown-button"></i> <div class="dropdown manual">
<div class="dropdown manual"> <div class="dropdown-display">
<div class="dropdown-display lg"> <div class="card ~neutral !low">
<div class="card ~neutral !low"> <span class="supra sm">${window.lang.strings("contactThrough")}</span>
<span class="supra sm">${window.lang.strings("contactThrough")}</span> <label class="switch pb-1 mt-half">
<label class="row switch pb-1 mt-half"> <input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-email">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email"> <span>Email</span>
<span>Email</span> </label>
</label> <label class="switch pb-1">
<label class="row switch pb-1"> <input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-discord">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord"> <span>Discord</span>
<span>Discord</span> </label>
</label>
`; `;
if (window.telegramEnabled && this._telegramUsername != "") { if (window.telegramEnabled && this._telegramUsername != "") {
innerHTML += ` innerHTML += `
<label class="row switch pb-1"> <label class="switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram"> <input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
<span>Telegram</span> <span>Telegram</span>
</label> </label>
`; `;
} }
innerHTML += ` innerHTML += `
</div>
</div> </div>
</div> </div>
</div> </div>
@ -327,8 +317,8 @@ class user implements User {
this._row = document.createElement("tr") as HTMLTableRowElement; this._row = document.createElement("tr") as HTMLTableRowElement;
let innerHTML = ` let innerHTML = `
<td><input type="checkbox" value=""></td> <td><input type="checkbox" value=""></td>
<td><div class="table-inline"><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></span></td> <td><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></td>
<td><div class="table-inline"><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></span></div></td> <td><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></span></td>
`; `;
if (window.telegramEnabled) { if (window.telegramEnabled) {
innerHTML += ` innerHTML += `
@ -411,76 +401,6 @@ class user implements User {
} }
}); });
} }
private _timer: NodeJS.Timer;
private _discordKbListener = () => {
clearTimeout(this._timer);
const list = document.getElementById("discord-list") as HTMLTableElement;
const input = document.getElementById("discord-search") as HTMLInputElement;
if (input.value.length < 2) {
return;
}
list.innerHTML = ``;
addLoader(list);
list.parentElement.classList.add("mb-1", "mt-1");
this._timer = setTimeout(() => {
_get("/users/discord/" + input.value, null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
removeLoader(list);
list.parentElement.classList.remove("mb-1", "mt-1");
return;
}
const users = req.response["users"] as Array<DiscordUser>;
let innerHTML = ``;
for (let i = 0; i < users.length; i++) {
innerHTML += `
<tr>
<td class="img-circle sm">
<img class="img-circle" src="${users[i].avatar_url}" width="32" height="32">
</td>
<td class="w-100 sm">
<p class="content">${users[i].name}</p>
</td>
<td class="sm">
<span id="discord-user-${users[i].id}" class="button ~info !high">${window.lang.strings("add")}</span>
</td>
</tr>
`;
}
list.innerHTML = innerHTML;
removeLoader(list);
list.parentElement.classList.remove("mb-1", "mt-1");
for (let i = 0; i < users.length; i++) {
const button = document.getElementById(`discord-user-${users[i].id}`) as HTMLInputElement;
button.onclick = () => _post("/users/discord", {jf_id: this.id, discord_id: users[i].id}, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
document.dispatchEvent(new CustomEvent("accounts-reload"));
if (req.status != 200) {
window.notifications.customError("errorConnectDiscord", window.lang.notif("errorFailureCheckLogs"));
return
}
window.notifications.customSuccess("discordConnected", window.lang.notif("accountConnected"));
window.modals.discord.close()
}
});
}
}
});
}, 750);
}
private _addDiscord = () => {
if (!window.discordEnabled) { return; }
const input = document.getElementById("discord-search") as HTMLInputElement;
const list = document.getElementById("discord-list") as HTMLDivElement;
list.innerHTML = ``;
input.value = "";
input.removeEventListener("keyup", this._discordKbListener);
input.addEventListener("keyup", this._discordKbListener);
window.modals.discord.show();
}
private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => { private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 200) { if (req.readyState == 4 && req.status == 200) {

View File

@ -102,7 +102,6 @@ declare interface Modals {
extendExpiry: Modal; extendExpiry: Modal;
updateInfo: Modal; updateInfo: Modal;
telegram: Modal; telegram: Modal;
discord: Modal;
} }
interface Invite { interface Invite {