mirror of
https://github.com/hrfee/jfa-go.git
synced 2025-01-06 00:10:11 +00:00
Compare commits
6 Commits
3bf722c5fe
...
e98c9b46f1
Author | SHA1 | Date | |
---|---|---|---|
e98c9b46f1 | |||
b3ce7acfcb | |||
9fac79b1f0 | |||
591e3c5ca1 | |||
35d407afef | |||
a6447165b7 |
105
api.go
105
api.go
@ -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.(string)
|
email = e.Addr
|
||||||
}
|
}
|
||||||
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] = req.Email
|
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
|
||||||
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] = req.Email
|
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
|
||||||
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 := app.storage.emails[gc.GetString("jfId")]; addr != nil {
|
if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" {
|
||||||
address = addr.(string)
|
address = addr.Addr
|
||||||
}
|
}
|
||||||
} 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) {
|
||||||
var ok bool
|
addr, ok := app.storage.emails[gc.GetString("jfId")]
|
||||||
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.(string)
|
user.Email = email.Addr
|
||||||
user.NotifyThroughEmail = user.Email != ""
|
user.NotifyThroughEmail = user.Email != ""
|
||||||
}
|
}
|
||||||
expiry, ok := app.storage.users[jfUser.ID]
|
expiry, ok := app.storage.users[jfUser.ID]
|
||||||
@ -1293,7 +1293,11 @@ 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 {
|
||||||
app.storage.emails[id] = address
|
contact := true
|
||||||
|
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 {
|
||||||
@ -2044,7 +2048,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 telegramNotifyDTO body telegramNotifyDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
|
// @Param SetContactMethodsDTO body SetContactMethodsDTO 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
|
||||||
@ -2052,7 +2056,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 telegramNotifyDTO
|
var req SetContactMethodsDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
if req.ID == "" {
|
if req.ID == "" {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
@ -2068,9 +2072,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
|
||||||
@ -2082,9 +2086,23 @@ 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)
|
||||||
}
|
}
|
||||||
@ -2146,7 +2164,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
|
||||||
// @Success 401 {object} boolResponse
|
// @Failure 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]
|
||||||
@ -2162,6 +2180,61 @@ 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
|
||||||
|
25
config.go
25
config.go
@ -171,3 +171,28 @@ 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
|
||||||
|
}
|
||||||
|
37
css/base.css
37
css/base.css
@ -30,12 +30,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 750px) {
|
@media screen and (max-width: 1000px) {
|
||||||
:root {
|
:root {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.table-responsive table {
|
.table-responsive table {
|
||||||
min-width: 660px;
|
min-width: 800px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,6 +130,10 @@ 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;
|
||||||
}
|
}
|
||||||
@ -424,6 +428,7 @@ p.top {
|
|||||||
|
|
||||||
.table-responsive {
|
.table-responsive {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#notification-box {
|
#notification-box {
|
||||||
@ -438,6 +443,10 @@ 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 */
|
||||||
@ -483,3 +492,27 @@ 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;
|
||||||
|
}
|
||||||
|
61
discord.go
61
discord.go
@ -41,6 +41,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +71,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
|
d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers
|
||||||
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
|
||||||
@ -91,6 +92,53 @@ 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"
|
||||||
@ -235,16 +283,7 @@ 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 != "" {
|
||||||
var links []Link
|
msg, embeds = StripAltText(message.Markdown, true)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
6
email.go
6
email.go
@ -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 {
|
if address, ok := app.storage.emails[id]; ok && address.Contact && emailEnabled {
|
||||||
err = app.email.send(email, address.(string))
|
err = app.email.send(email, address.Addr)
|
||||||
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.(string)
|
return addr.Addr
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -328,6 +328,18 @@
|
|||||||
</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">×</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">
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
"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",
|
||||||
@ -94,7 +95,8 @@
|
|||||||
"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}.",
|
||||||
@ -107,6 +109,7 @@
|
|||||||
"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
143
main.go
@ -16,7 +16,6 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -321,6 +320,10 @@ 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 {
|
||||||
@ -434,76 +437,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
|
||||||
|
17
models.go
17
models.go
@ -255,9 +255,24 @@ type telegramSetDTO struct {
|
|||||||
ID string `json:"id"` // Jellyfin ID of user.
|
ID string `json:"id"` // Jellyfin ID of user.
|
||||||
}
|
}
|
||||||
|
|
||||||
type telegramNotifyDTO struct {
|
type SetContactMethodsDTO 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"`
|
||||||
|
}
|
||||||
|
@ -161,12 +161,16 @@ 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 {
|
if telegramEnabled || discordEnabled {
|
||||||
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)
|
||||||
|
172
storage.go
172
storage.go
@ -21,7 +21,8 @@ type Storage struct {
|
|||||||
invites Invites
|
invites Invites
|
||||||
profiles map[string]Profile
|
profiles map[string]Profile
|
||||||
defaultProfile string
|
defaultProfile string
|
||||||
emails, displayprefs, ombi_template map[string]interface{}
|
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
|
||||||
@ -47,6 +48,11 @@ 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"`
|
||||||
@ -902,85 +908,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
|
||||||
}
|
// }
|
||||||
|
23
stripmd.go
23
stripmd.go
@ -3,6 +3,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -13,22 +14,32 @@ 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, []Link) {
|
func StripAltText(md string, links bool) (string, []*dg.MessageEmbed) {
|
||||||
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 := []Link{}
|
embeds := []*dg.MessageEmbed{}
|
||||||
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 {
|
||||||
embeds = append(embeds, Link{
|
embed := &dg.MessageEmbed{
|
||||||
URL: md[URLStart : URLEnd+1],
|
Type: dg.EmbedTypeLink,
|
||||||
Alt: md[altTextStart : URLStart-2],
|
Title: 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]
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,10 @@ 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();
|
||||||
|
@ -24,6 +24,12 @@ 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;
|
||||||
@ -96,7 +102,9 @@ 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;
|
||||||
email.checked = s;
|
if (email) {
|
||||||
|
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;
|
||||||
@ -114,52 +122,50 @@ 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 += `
|
||||||
<label class="switch pb-1">
|
<div class="table-inline">
|
||||||
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord">
|
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
|
||||||
<span>Discord</span>
|
<div class="dropdown manual">
|
||||||
</label>
|
<div class="dropdown-display lg">
|
||||||
`;
|
<div class="card ~neutral !low">
|
||||||
}
|
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
|
||||||
innerHTML += `
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
`;
|
||||||
`;
|
|
||||||
this._discord.innerHTML = innerHTML;
|
|
||||||
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
|
|
||||||
const button = this._telegram.querySelector("i");
|
|
||||||
const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
|
|
||||||
const checks = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
|
||||||
for (let i = 0; i < checks.length; i++) {
|
|
||||||
checks[i].onclick = () => this._setNotifyMethod("telegram");
|
|
||||||
}
|
}
|
||||||
|
this._telegram.innerHTML = innerHTML;
|
||||||
button.onclick = () => {
|
if (!window.discordEnabled || this._discordUsername == "") {
|
||||||
dropdown.classList.add("selected");
|
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
|
||||||
document.addEventListener("click", outerClickListener);
|
const button = this._telegram.querySelector("i");
|
||||||
};
|
const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
|
||||||
const outerClickListener = (event: Event) => {
|
const checks = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
||||||
if (!(event.target instanceof HTMLElement && (this._telegram.contains(event.target) || button.contains(event.target)))) {
|
for (let i = 0; i < checks.length; i++) {
|
||||||
dropdown.classList.remove("selected");
|
checks[i].onclick = () => this._setNotifyMethod("telegram");
|
||||||
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +174,9 @@ 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;
|
||||||
telegram.checked = s;
|
if (telegram) {
|
||||||
|
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;
|
||||||
@ -218,33 +226,35 @@ 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 = `
|
||||||
<a href="https://discord.com/users/${this._discordID}" class="discord-link" target="_blank">@${u}</a>
|
<div class="table-inline">
|
||||||
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
|
<a href="https://discord.com/users/${this._discordID}" class="discord-link" target="_blank">${u}</a>
|
||||||
<div class="dropdown manual">
|
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
|
||||||
<div class="dropdown-display">
|
<div class="dropdown manual">
|
||||||
<div class="card ~neutral !low">
|
<div class="dropdown-display lg">
|
||||||
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
|
<div class="card ~neutral !low">
|
||||||
<label class="switch pb-1 mt-half">
|
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
|
||||||
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-email">
|
<label class="row switch pb-1 mt-half">
|
||||||
<span>Email</span>
|
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
|
||||||
</label>
|
<span>Email</span>
|
||||||
<label class="switch pb-1">
|
</label>
|
||||||
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-discord">
|
<label class="row switch pb-1">
|
||||||
<span>Discord</span>
|
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord">
|
||||||
</label>
|
<span>Discord</span>
|
||||||
|
</label>
|
||||||
`;
|
`;
|
||||||
if (window.telegramEnabled && this._telegramUsername != "") {
|
if (window.telegramEnabled && this._telegramUsername != "") {
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
<label class="switch pb-1">
|
<label class="row switch pb-1">
|
||||||
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
|
<input type="checkbox" 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>
|
||||||
@ -317,8 +327,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><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></td>
|
<td><div class="table-inline"><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></span></td>
|
||||||
<td><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></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>
|
||||||
`;
|
`;
|
||||||
if (window.telegramEnabled) {
|
if (window.telegramEnabled) {
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
@ -401,6 +411,76 @@ 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) {
|
||||||
|
@ -102,6 +102,7 @@ declare interface Modals {
|
|||||||
extendExpiry: Modal;
|
extendExpiry: Modal;
|
||||||
updateInfo: Modal;
|
updateInfo: Modal;
|
||||||
telegram: Modal;
|
telegram: Modal;
|
||||||
|
discord: Modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Invite {
|
interface Invite {
|
||||||
|
Loading…
Reference in New Issue
Block a user