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.
|
||||
// @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
|
||||
|
23
css/base.css
23
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;
|
||||
}
|
||||
|
56
discord.go
56
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 == "" {
|
||||
|
@ -328,6 +328,18 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ 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>
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
|
@ -20,6 +20,7 @@
|
||||
"create": "Create",
|
||||
"apply": "Apply",
|
||||
"delete": "Delete",
|
||||
"add": "Add",
|
||||
"name": "Name",
|
||||
"date": "Date",
|
||||
"enabled": "Enabled",
|
||||
@ -94,7 +95,8 @@
|
||||
"notifyEvent": "Notify on:",
|
||||
"notifyInviteExpiry": "On expiry",
|
||||
"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": {
|
||||
"changedEmailAddress": "Changed email address of {n}.",
|
||||
@ -107,6 +109,7 @@
|
||||
"updateApplied": "Update applied, please restart.",
|
||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||
"telegramVerified": "Telegram account verified.",
|
||||
"accountConnected": "Account connected.",
|
||||
"errorConnection": "Couldn't connect to jfa-go.",
|
||||
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
||||
"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"`
|
||||
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/verified/:pin", app.TelegramVerified)
|
||||
api.POST(p+"/users/telegram", app.TelegramAddUser)
|
||||
}
|
||||
if discordEnabled || telegramEnabled {
|
||||
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) {
|
||||
api.GET(p+"/ombi/users", app.OmbiUsers)
|
||||
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)
|
||||
|
@ -66,6 +66,10 @@ window.availableProfiles = window.availableProfiles || [];
|
||||
if (window.telegramEnabled) {
|
||||
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();
|
||||
|
@ -24,6 +24,12 @@ interface getPinResponse {
|
||||
username: string;
|
||||
}
|
||||
|
||||
interface DiscordUser {
|
||||
name: string;
|
||||
avatar_url: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
class user implements User {
|
||||
private _row: HTMLTableRowElement;
|
||||
private _check: HTMLInputElement;
|
||||
@ -218,7 +224,7 @@ class user implements User {
|
||||
this._discordUsername = u;
|
||||
if (u == "") {
|
||||
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 {
|
||||
let innerHTML = `
|
||||
<a href="https://discord.com/users/${this._discordID}" class="discord-link" target="_blank">${u}</a>
|
||||
@ -402,6 +408,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) => {
|
||||
if (req.readyState == 4 && req.status == 200) {
|
||||
const pin = document.getElementById("telegram-pin");
|
||||
|
@ -102,6 +102,7 @@ declare interface Modals {
|
||||
extendExpiry: Modal;
|
||||
updateInfo: Modal;
|
||||
telegram: Modal;
|
||||
discord: Modal;
|
||||
}
|
||||
|
||||
interface Invite {
|
||||
|
Loading…
Reference in New Issue
Block a user