diff --git a/api.go b/api.go index f42f7158..b3d08790 100644 --- a/api.go +++ b/api.go @@ -2048,7 +2048,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) { // @Summary Sets whether to notify a user through telegram or not. // @Produce json -// @Param SetContactMethodDTO body SetContactMethodsDTO 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 400 {object} boolResponse // @Success 500 {object} boolResponse @@ -2164,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. // @Produce json // @Success 200 {object} boolResponse -// @Success 401 {object} boolResponse +// @Failure 401 {object} boolResponse // @Param pin path string true "PIN code to check" // @Param invCode path string true "invite Code" // @Router /invite/{invCode}/discord/verified/{pin} [get] @@ -2180,6 +2180,61 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) { 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. // @Router /restart [post] // @Security Bearer diff --git a/css/base.css b/css/base.css index 3b9626de..c4889b65 100644 --- a/css/base.css +++ b/css/base.css @@ -130,6 +130,10 @@ div.card:contains(section.banner.footer) { text-align: center; } +.w-100 { + width: 100%; +} + .inline-block { display: inline-block; } @@ -483,3 +487,22 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) { max-width: 15rem; 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; +} diff --git a/discord.go b/discord.go index 4c449315..3d92ead1 100644 --- a/discord.go +++ b/discord.go @@ -71,7 +71,7 @@ func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string) func (d *DiscordDaemon) run() { 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 { d.app.err.Printf("Discord: Failed to start daemon: %v", err) return @@ -92,6 +92,60 @@ func (d *DiscordDaemon) run() { 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() { + d.Stopped = true + d.ShutdownChannel <- "Down" + <-d.ShutdownChannel + close(d.ShutdownChannel) +} + func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) { if m.GuildID != "" && d.channelName != "" { if d.channelID == "" { diff --git a/html/admin.html b/html/admin.html index 62861447..bc0f80db 100644 --- a/html/admin.html +++ b/html/admin.html @@ -328,6 +328,18 @@
{{ end }} + {{ if .discord_enabled }} +