mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 09:00:10 +00:00
Discord: Add users via accounts tab
Doesn't require a PIN like Telegram, as we can access a list of guild users with the GuildMembers intent set. This has to be enabled under Bot > Priviliged Gateway intents on the developer portal.
This commit is contained in:
parent
591e3c5ca1
commit
9fac79b1f0
59
api.go
59
api.go
@ -2048,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 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 200 {object} boolResponse
|
||||||
// @Success 400 {object} boolResponse
|
// @Success 400 {object} boolResponse
|
||||||
// @Success 500 {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.
|
// @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]
|
||||||
@ -2180,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
|
||||||
|
23
css/base.css
23
css/base.css
@ -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;
|
||||||
}
|
}
|
||||||
@ -483,3 +487,22 @@ 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;
|
||||||
|
}
|
||||||
|
56
discord.go
56
discord.go
@ -71,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
|
||||||
@ -92,6 +92,60 @@ 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() {
|
||||||
|
d.Stopped = true
|
||||||
|
d.ShutdownChannel <- "Down"
|
||||||
|
<-d.ShutdownChannel
|
||||||
|
close(d.ShutdownChannel)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
|
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
|
||||||
if m.GuildID != "" && d.channelName != "" {
|
if m.GuildID != "" && d.channelName != "" {
|
||||||
if d.channelID == "" {
|
if d.channelID == "" {
|
||||||
|
@ -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.",
|
||||||
|
15
models.go
15
models.go
@ -261,3 +261,18 @@ type SetContactMethodsDTO struct {
|
|||||||
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"`
|
||||||
|
}
|
||||||
|
@ -165,10 +165,12 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
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)
|
||||||
}
|
|
||||||
if discordEnabled || telegramEnabled {
|
|
||||||
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)
|
||||||
|
@ -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;
|
||||||
@ -218,7 +224,7 @@ 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>
|
<a href="https://discord.com/users/${this._discordID}" class="discord-link" target="_blank">${u}</a>
|
||||||
@ -401,6 +407,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