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

Compare commits

...

3 Commits

Author SHA1 Message Date
375022ba95
Matrix: Add token generation wizard
Pressing the "+" next to matrix in settings allows you to enter a
homeserver, username and password to enable matrix and generate an
access token.
2021-05-30 23:09:20 +01:00
75fdf6ec3d
Matrix: Connect on accounts tab, customizable chat topic 2021-05-30 11:47:41 +01:00
59ebf52fe2
Matrix: Show matrix on accounts page 2021-05-30 00:05:46 +01:00
13 changed files with 450 additions and 147 deletions

141
api.go
View File

@ -1277,10 +1277,14 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.Telegram = tgUser.Username
user.NotifyThroughTelegram = tgUser.Contact
}
if dc, ok := app.storage.discord[jfUser.ID]; ok {
user.Discord = dc.Username + "#" + dc.Discriminator
user.DiscordID = dc.ID
user.NotifyThroughDiscord = dc.Contact
if mxUser, ok := app.storage.matrix[jfUser.ID]; ok {
user.Matrix = mxUser.UserID
user.NotifyThroughMatrix = mxUser.Contact
}
if dcUser, ok := app.storage.discord[jfUser.ID]; ok {
user.Discord = dcUser.Username + "#" + dcUser.Discriminator
user.DiscordID = dcUser.ID
user.NotifyThroughDiscord = dcUser.Contact
}
resp.UserList[i] = user
i++
@ -1536,6 +1540,8 @@ func (app *appContext) GetConfig(gc *gin.Context) {
resp.Sections["email"].Settings["language"] = el
resp.Sections["password_resets"].Settings["language"] = pl
resp.Sections["telegram"].Settings["language"] = tl
resp.Sections["discord"].Settings["language"] = tl
resp.Sections["matrix"].Settings["language"] = tl
gc.JSON(200, resp)
}
@ -1544,6 +1550,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
// @Produce json
// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings."
// @Success 200 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /config [post]
// @Security Bearer
// @tags Configuration
@ -1569,7 +1576,11 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
}
}
}
tempConfig.SaveTo(app.configPath)
if err := tempConfig.SaveTo(app.configPath); err != nil {
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
respondBool(500, false, gc)
return
}
app.debug.Println("Config saved")
gc.JSON(200, map[string]bool{"success": true})
if req["restart-program"] != nil && req["restart-program"].(bool) {
@ -2127,6 +2138,7 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
return
}
if tgUser, ok := app.storage.telegram[req.ID]; ok {
change := tgUser.Contact != req.Telegram
tgUser.Contact = req.Telegram
app.storage.telegram[req.ID] = tgUser
if err := app.storage.storeTelegramUsers(); err != nil {
@ -2134,13 +2146,16 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
app.err.Printf("Telegram: Failed to store users: %v", err)
return
}
msg := ""
if !req.Telegram {
msg = " not"
if change {
msg := ""
if !req.Telegram {
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 {
change := dcUser.Contact != req.Discord
dcUser.Contact = req.Discord
app.storage.discord[req.ID] = dcUser
if err := app.storage.storeDiscordUsers(); err != nil {
@ -2148,13 +2163,33 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
app.err.Printf("Discord: Failed to store users: %v", err)
return
}
msg := ""
if !req.Discord {
msg = " not"
if change {
msg := ""
if !req.Discord {
msg = " not"
}
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
}
}
if mxUser, ok := app.storage.matrix[req.ID]; ok {
change := mxUser.Contact != req.Matrix
mxUser.Contact = req.Matrix
app.storage.matrix[req.ID] = mxUser
if err := app.storage.storeMatrixUsers(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Matrix: Failed to store users: %v", err)
return
}
if change {
msg := ""
if !req.Matrix {
msg = " not"
}
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, 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 {
change := email.Contact != req.Email
email.Contact = req.Email
app.storage.emails[req.ID] = email
if err := app.storage.storeEmails(); err != nil {
@ -2162,11 +2197,13 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
app.err.Printf("Failed to store emails: %v", err)
return
}
msg := ""
if !req.Email {
msg = " not"
if change {
msg := ""
if !req.Email {
msg = " not"
}
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
}
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
}
respondBool(200, true, gc)
}
@ -2335,6 +2372,76 @@ func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
respondBool(200, true, gc)
}
// @Summary Generates a Matrix access token from a username and password.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param MatrixLoginDTO body MatrixLoginDTO true "Username & password."
// @Router /matrix/login [post]
// @tags Other
func (app *appContext) MatrixLogin(gc *gin.Context) {
var req MatrixLoginDTO
gc.BindJSON(&req)
if req.Username == "" || req.Password == "" {
respond(400, "errorLoginBlank", gc)
return
}
token, err := app.matrix.generateAccessToken(req.Homeserver, req.Username, req.Password)
if err != nil {
app.err.Printf("Matrix: Failed to generate token: %v", err)
respond(401, "Unauthorized", gc)
return
}
tempConfig, _ := ini.Load(app.configPath)
matrix := tempConfig.Section("matrix")
matrix.Key("enabled").SetValue("true")
matrix.Key("homeserver").SetValue(req.Homeserver)
matrix.Key("token").SetValue(token)
matrix.Key("user_id").SetValue(req.Username)
if err := tempConfig.SaveTo(app.configPath); err != nil {
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Links a Matrix user 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 MatrixConnectUserDTO body MatrixConnectUserDTO true "User's Jellyfin ID & Matrix user ID."
// @Router /users/matrix [post]
// @tags Other
func (app *appContext) MatrixConnect(gc *gin.Context) {
var req MatrixConnectUserDTO
gc.BindJSON(&req)
if app.storage.matrix == nil {
app.storage.matrix = map[string]MatrixUser{}
}
roomID, err := app.matrix.CreateRoom(req.UserID)
if err != nil {
app.err.Printf("Matrix: Failed to create room: %v", err)
respondBool(500, false, gc)
return
}
app.storage.matrix[req.JellyfinID] = MatrixUser{
UserID: req.UserID,
RoomID: roomID,
Lang: "en-us",
Contact: true,
}
if err := app.storage.storeMatrixUsers(); err != nil {
app.err.Printf("Failed to store Matrix users: %v", err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional).
// @Produce json
// @Success 200 {object} DiscordUsersDTO

View File

@ -84,6 +84,8 @@ func (app *appContext) loadConfig() error {
app.MustSetValue("user_expiry", "email_html", "jfa-go:"+"user-expired.html")
app.MustSetValue("user_expiry", "email_text", "jfa-go:"+"user-expired.txt")
app.MustSetValue("matrix", "topic", "Jellyfin notifications")
app.config.Section("jellyfin").Key("version").SetValue(version)
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))

View File

@ -727,6 +727,15 @@
"value": "",
"description": "User ID of bot account (Example: @jfa-bot:riot.im)"
},
"topic": {
"name": "Chat topic",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "text",
"value": "Jellyfin notifications",
"description": "Topic of Matrix private chats."
},
"language": {
"name": "Language",
"required": false,

View File

@ -8,6 +8,7 @@
window.emailEnabled = {{ .email_enabled }};
window.telegramEnabled = {{ .telegram_enabled }};
window.discordEnabled = {{ .discord_enabled }};
window.matrixEnabled = {{ .matrix_enabled }};
window.ombiEnabled = {{ .ombiEnabled }};
window.usernameEnabled = {{ .username }};
window.langFile = JSON.parse({{ .language }});
@ -340,6 +341,19 @@
</div>
</div>
{{ end }}
<div id="modal-matrix" class="modal">
<form class="modal-content card" id="form-matrix" href="">
<span class="heading">{{ .strings.linkMatrix }}</span>
<p class="content">{{ .strings.linkMatrixDescription }}</p>
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.matrixHomeServer }}" id="matrix-homeserver">
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.username }}" id="matrix-user">
<input type="password" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.password }}" id="matrix-password">
<label>
<input type="submit" class="unfocused">
<span class="button ~urge !normal full-width center supra submit">{{ .strings.submit }}</span>
</label>
</form>
</div>
<div id="notification-box"></div>
<span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
@ -545,6 +559,9 @@
{{ if .telegram_enabled }}
<th>Telegram</th>
{{ end }}
{{ if .matrix_enabled }}
<th>Matrix</th>
{{ end }}
{{ if .discord_enabled }}
<th>Discord</th>
{{ end }}

View File

@ -98,7 +98,9 @@
"notifyUserCreation": "On user creation",
"sendPIN": "Ask the user to send the PIN below to the bot.",
"searchDiscordUser": "Start typing the Discord username to find the user.",
"findDiscordUser": "Find Discord user"
"findDiscordUser": "Find Discord user",
"linkMatrixDescription": "Enter the username and password of the user to use as a bot. Once submitted, the app will restart.",
"matrixHomeServer": "Home server address"
},
"notifications": {
"changedEmailAddress": "Changed email address of {n}.",

View File

@ -31,6 +31,13 @@ type MatrixUser struct {
Contact bool
}
type MatrixIdentifier struct {
User string `json:"user"`
IdentType string `json:"type"`
}
func (m MatrixIdentifier) Type() string { return m.IdentType }
var matrixFilter = gomatrix.Filter{
Room: gomatrix.RoomFilter{
Timeline: gomatrix.FilterPart{
@ -80,6 +87,27 @@ func newMatrixDaemon(app *appContext) (d *MatrixDaemon, err error) {
return
}
func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string) (string, error) {
req := &gomatrix.ReqLogin{
Type: "m.login.password",
Identifier: MatrixIdentifier{
User: username,
IdentType: "m.id.user",
},
Password: password,
DeviceID: "jfa-go-" + commit,
}
bot, err := gomatrix.NewClient(homeserver, username, "")
if err != nil {
return "", err
}
resp, err := bot.Login(req)
if err != nil {
return "", err
}
return resp.AccessToken, nil
}
func (d *MatrixDaemon) run() {
d.app.info.Println("Starting Matrix bot daemon")
syncer := d.bot.Syncer.(*gomatrix.DefaultSyncer)
@ -145,12 +173,20 @@ func (d *MatrixDaemon) commandLang(event *gomatrix.Event, code, lang string) {
}
}
func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
func (d *MatrixDaemon) CreateRoom(userID string) (string, error) {
room, err := d.bot.CreateRoom(&gomatrix.ReqCreateRoom{
Visibility: "private",
Invite: []string{userID},
Topic: "jfa-go",
Topic: d.app.config.Section("matrix").Key("topic").String(),
})
if err != nil {
return "", err
}
return room.RoomID, nil
}
func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
roomID, err := d.CreateRoom(userID)
if err != nil {
d.app.err.Printf("Failed to create room for user \"%s\": %v", userID, err)
return
@ -160,13 +196,13 @@ func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
d.tokens[pin] = UnverifiedUser{
false,
&MatrixUser{
RoomID: room.RoomID,
RoomID: roomID,
UserID: userID,
Lang: lang,
},
}
_, err = d.bot.SendText(
room.RoomID,
roomID,
d.app.storage.lang.Telegram[lang].Strings.get("matrixStartMessage")+"\n\n"+pin+"\n\n"+
d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"}),
)

View File

@ -139,6 +139,8 @@ type respUser struct {
Discord string `json:"discord"` // Discord username (if known)
DiscordID string `json:"discord_id"` // Discord user ID for creating links.
NotifyThroughDiscord bool `json:"notify_discord"`
Matrix string `json:"matrix"` // Matrix ID (if known)
NotifyThroughMatrix bool `json:"notify_matrix"`
}
type getUsersDTO struct {
@ -262,6 +264,7 @@ type SetContactMethodsDTO struct {
Email bool `json:"email"`
Discord bool `json:"discord"`
Telegram bool `json:"telegram"`
Matrix bool `json:"matrix"`
}
type DiscordUserDTO struct {
@ -287,6 +290,18 @@ type DiscordInviteDTO struct {
type MatrixSendPINDTO struct {
UserID string `json:"user_id"`
}
type MatrixCheckPINDTO struct {
PIN string `json:"pin"`
}
type MatrixConnectUserDTO struct {
JellyfinID string `json:"jf_id"`
UserID string `json:"user_id"`
}
type MatrixLoginDTO struct {
Homeserver string `json:"homeserver"`
Username string `json:"username"`
Password string `json:"password"`
}

View File

@ -130,6 +130,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
if matrixEnabled {
router.GET(p+"/invite/:invCode/matrix/verified/:userID/:pin", app.MatrixCheckPIN)
router.POST(p+"/invite/:invCode/matrix/user", app.MatrixSendPIN)
router.POST(p+"/users/matrix", app.MatrixConnect)
}
}
if *SWAGGER {
@ -168,7 +169,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/config", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart)
if telegramEnabled || discordEnabled {
if telegramEnabled || discordEnabled || matrixEnabled {
api.GET(p+"/telegram/pin", app.TelegramGetPin)
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
api.POST(p+"/users/telegram", app.TelegramAddUser)
@ -182,6 +183,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/ombi/users", app.OmbiUsers)
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)
}
api.POST(p+"/matrix/login", app.MatrixLogin)
}
}

View File

@ -63,6 +63,8 @@ window.availableProfiles = window.availableProfiles || [];
window.modals.updateInfo = new Modal(document.getElementById("modal-update"));
window.modals.matrix = new Modal(document.getElementById("modal-matrix"));
if (window.telegramEnabled) {
window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
}

View File

@ -18,6 +18,8 @@ interface User {
discord: string;
notify_discord: boolean;
discord_id: string;
matrix: string;
notify_matrix: boolean;
}
interface getPinResponse {
@ -44,13 +46,27 @@ class user implements User {
private _discordUsername: string;
private _discordID: string;
private _notifyDiscord: boolean;
private _matrix: HTMLTableDataCellElement;
private _matrixID: string;
private _notifyMatrix: boolean;
private _expiry: HTMLTableDataCellElement;
private _expiryUnix: number;
private _lastActive: HTMLTableDataCellElement;
private _lastActiveUnix: number;
private _notifyDropdown: HTMLDivElement;
id = "";
private _selected: boolean;
private _lastNotifyMethod = (): string => {
// Telegram, Matrix, Discord
const telegram = this._telegramUsername && this._telegramUsername != "";
const discord = this._discordUsername && this._discordUsername != "";
const matrix = this._matrixID && this._matrixID != "";
if (discord) return "discord";
if (matrix) return "matrix";
if (telegram) return "telegram";
}
get selected(): boolean { return this._selected; }
set selected(state: boolean) {
this._selected = state;
@ -96,105 +112,179 @@ class user implements User {
get notify_email(): boolean { return this._notifyEmail; }
set notify_email(s: boolean) {
this._notifyEmail = s;
if (window.telegramEnabled && this._telegramUsername != "") {
const email = this._telegram.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
if (email) {
email.checked = s;
if (this._notifyDropdown) {
(this._notifyDropdown.querySelector(".accounts-contact-email") as HTMLInputElement).checked = s;
}
}
private _constructDropdown = (): HTMLDivElement => {
const el = document.createElement("div") as HTMLDivElement;
const telegram = this._telegramUsername != "";
const discord = this._discordUsername != "";
const matrix = this._matrixID != "";
if (!telegram && !discord && !matrix) return;
let innerHTML = `
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
<div class="dropdown manual">
<div class="dropdown-display lg">
<div class="card ~neutral !low">
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
<label class="row switch pb-1 mt-half">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
</span>Email</span>
</label>
<div class="accounts-area-telegram">
<label class="row switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
<span>Telegram</span>
</label>
</div>
<div class="accounts-area-discord">
<label class="row switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord">
<span>Discord</span>
</label>
</div>
<div class="accounts-area-matrix">
<label class="row switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-matrix">
<span>Matrix</span>
</label>
</div>
</div>
</div>
</div>
`;
el.innerHTML = innerHTML;
const button = el.querySelector("i");
const dropdown = el.querySelector("div.dropdown") as HTMLDivElement;
const checks = el.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
for (let i = 0; i < checks.length; i++) {
checks[i].onclick = () => this._setNotifyMethod();
}
button.onclick = () => {
dropdown.classList.add("selected");
document.addEventListener("click", outerClickListener);
};
const outerClickListener = (event: Event) => {
if (!(event.target instanceof HTMLElement && (el.contains(event.target) || button.contains(event.target)))) {
dropdown.classList.remove("selected");
document.removeEventListener("click", outerClickListener);
}
};
return el;
}
get matrix(): string { return this._matrixID; }
set matrix(u: string) {
if (!window.matrixEnabled) {
this._notifyDropdown.querySelector(".accounts-area-matrix").classList.add("unfocused");
return;
}
const lastNotifyMethod = this._lastNotifyMethod() == "matrix";
this._matrixID = u;
if (!u) {
this._notifyDropdown.querySelector(".accounts-area-matrix").classList.add("unfocused");
this._matrix.innerHTML = `
<span class="chip btn !low">${window.lang.strings("add")}</span>
<input type="text" class="input ~neutral !normal stealth-input unfocused" placeholder="@user:riot.im">
`;
(this._matrix.querySelector("span") as HTMLSpanElement).onclick = this._addMatrix;
} else {
this._notifyDropdown.querySelector(".accounts-area-matrix").classList.remove("unfocused");
this._matrix.innerHTML = `
<div class="table-inline">
${u}
</div>
`;
if (lastNotifyMethod) {
(this._matrix.querySelector(".table-inline") as HTMLDivElement).appendChild(this._notifyDropdown);
}
}
if (window.discordEnabled && this._discordUsername) {
const email = this._discord.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
email.checked = s;
}
private _addMatrix = () => {
const addButton = this._matrix.querySelector(".btn") as HTMLSpanElement;
const icon = this._matrix.querySelector("i");
const input = this._matrix.querySelector("input.stealth-input") as HTMLInputElement;
if (addButton.classList.contains("chip")) {
input.classList.remove("unfocused");
addButton.innerHTML = `<i class="ri-check-line"></i>`;
addButton.classList.remove("chip")
if (icon) {
icon.classList.add("unfocused");
}
} else {
if (input.value.charAt(0) != "@" || !input.value.includes(":")) return;
const send = {
jf_id: this.id,
user_id: input.value
}
_post("/users/matrix", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
document.dispatchEvent(new CustomEvent("accounts-reload"));
if (req.status != 200) {
window.notifications.customError("errorConnectMatrix", window.lang.notif("errorFailureCheckLogs"));
return;
}
window.notifications.customSuccess("connectMatrix", window.lang.notif("accountConnected"));
}
});
}
}
get notify_matrix(): boolean { return this._notifyMatrix; }
set notify_matrix(s: boolean) {
if (this._notifyDropdown) {
(this._notifyDropdown.querySelector(".accounts-contact-matrix") as HTMLInputElement).checked = s;
}
}
get telegram(): string { return this._telegramUsername; }
set telegram(u: string) {
if (!window.telegramEnabled) return;
if (!window.telegramEnabled) {
this._notifyDropdown.querySelector(".accounts-area-telegram").classList.add("unfocused");
return;
}
const lastNotifyMethod = this._lastNotifyMethod() == "telegram";
this._telegramUsername = u;
if (u == "") {
this._telegram.innerHTML = `<span class="chip btn !low">Add</span>`;
if (!u) {
this._notifyDropdown.querySelector(".accounts-area-telegram").classList.add("unfocused");
this._telegram.innerHTML = `<span class="chip btn !low">${window.lang.strings("add")}</span>`;
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
} else {
let innerHTML = `
this._notifyDropdown.querySelector(".accounts-area-telegram").classList.remove("unfocused");
this._telegram.innerHTML = `
<div class="table-inline">
<a href="https://t.me/${u}" target="_blank">@${u}</a>
</div>
`;
if (!window.discordEnabled || !this._discordUsername) {
innerHTML += `
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
<div class="dropdown manual">
<div class="dropdown-display lg">
<div class="card ~neutral !low">
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
<label class="row switch pb-1 mt-half">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
<span>Email</span>
</label>
<label class="row switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
<span>Telegram</span>
</label>
</div>
</div>
</div>
`;
}
innerHTML += "</div>";
this._telegram.innerHTML = innerHTML;
if (!window.discordEnabled || !this._discordUsername) {
// 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");
}
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);
}
};
if (lastNotifyMethod) {
(this._telegram.querySelector(".table-inline") as HTMLDivElement).appendChild(this._notifyDropdown);
}
}
}
get notify_telegram(): boolean { return this._notifyTelegram; }
set notify_telegram(s: boolean) {
if (!window.telegramEnabled || !this._telegramUsername) return;
this._notifyTelegram = s;
const telegram = this._telegram.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
if (telegram) {
telegram.checked = s;
}
if (window.discordEnabled && this._discordUsername) {
const telegram = this._discord.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
telegram.checked = s;
if (this._notifyDropdown) {
(this._notifyDropdown.querySelector(".accounts-contact-telegram") as HTMLInputElement).checked = s;
}
}
private _setNotifyMethod = (mode: string = "telegram") => {
let el: HTMLElement;
if (mode == "telegram") { el = this._telegram }
else if (mode == "discord") { el = this._discord }
const email = el.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
private _setNotifyMethod = () => {
const email = this._notifyDropdown.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
let send = {
id: this.id,
email: email.checked
}
if (window.telegramEnabled && this._telegramUsername) {
const telegram = el.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
const telegram = this._notifyDropdown.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
send["telegram"] = telegram.checked;
}
if (window.discordEnabled && this._discordUsername) {
const discord = el.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
const discord = this._notifyDropdown.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
send["discord"] = discord.checked;
}
_post("/users/contact", send, (req: XMLHttpRequest) => {
@ -219,62 +309,26 @@ class user implements User {
get discord(): string { return this._discordUsername; }
set discord(u: string) {
if (!window.discordEnabled) return;
if (!window.discordEnabled) {
this._notifyDropdown.querySelector(".accounts-area-discord").classList.add("unfocused");
return;
}
const lastNotifyMethod = this._lastNotifyMethod() == "discord";
this._discordUsername = u;
if (u == "") {
if (!u) {
this._discord.innerHTML = `<span class="chip btn !low">Add</span>`;
(this._discord.querySelector("span") as HTMLSpanElement).onclick = () => addDiscord(this.id);
this._notifyDropdown.querySelector(".accounts-area-discord").classList.add("unfocused");
} else {
let innerHTML = `
this._notifyDropdown.querySelector(".accounts-area-discord").classList.remove("unfocused");
this._discord.innerHTML = `
<div class="table-inline">
<a href="https://discord.com/users/${this._discordID}" class="discord-link" target="_blank">${u}</a>
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
<div class="dropdown manual">
<div class="dropdown-display lg">
<div class="card ~neutral !low">
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
<label class="row switch pb-1 mt-half">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
<span>Email</span>
</label>
<label class="row switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord">
<span>Discord</span>
</label>
`;
if (window.telegramEnabled && this._telegramUsername != "") {
innerHTML += `
<label class="row switch pb-1">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
<span>Telegram</span>
</label>
`;
}
innerHTML += `
</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._discord.querySelector("i");
const dropdown = this._discord.querySelector("div.dropdown") as HTMLDivElement;
const checks = this._discord.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
for (let i = 0; i < checks.length; i++) {
checks[i].onclick = () => this._setNotifyMethod("discord");
if (lastNotifyMethod) {
(this._discord.querySelector(".table-inline") as HTMLDivElement).appendChild(this._notifyDropdown);
}
button.onclick = () => {
dropdown.classList.add("selected");
document.addEventListener("click", outerClickListener);
};
const outerClickListener = (event: Event) => {
if (!(event.target instanceof HTMLElement && (this._discord.contains(event.target) || button.contains(event.target)))) {
dropdown.classList.remove("selected");
document.removeEventListener("click", outerClickListener);
}
};
}
}
@ -288,13 +342,8 @@ class user implements User {
get notify_discord(): boolean { return this._notifyDiscord; }
set notify_discord(s: boolean) {
if (!window.discordEnabled || !this._discordUsername) return;
this._notifyDiscord = s;
const discord = this._discord.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
discord.checked = s;
if (window.telegramEnabled && this._telegramUsername != "") {
const discord = this._discord.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
discord.checked = s;
if (this._notifyDropdown) {
(this._notifyDropdown.querySelector(".accounts-contact-discord") as HTMLInputElement).checked = s;
}
}
@ -333,6 +382,11 @@ class user implements User {
<td class="accounts-telegram"></td>
`;
}
if (window.matrixEnabled) {
innerHTML += `
<td class="accounts-matrix"></td>
`;
}
if (window.discordEnabled) {
innerHTML += `
<td class="accounts-discord"></td>
@ -352,10 +406,13 @@ class user implements User {
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement;
this._discord = this._row.querySelector(".accounts-discord") as HTMLTableDataCellElement;
this._matrix = this._row.querySelector(".accounts-matrix") as HTMLTableDataCellElement;
this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
this._check.onchange = () => { this.selected = this._check.checked; }
this._notifyDropdown = this._constructDropdown();
const toggleStealthInput = () => {
if (this._emailEditButton.classList.contains("ri-edit-line")) {
this._email.innerHTML = emailEditor;
@ -458,14 +515,20 @@ class user implements User {
this.id = user.id;
this.name = user.name;
this.email = user.email || "";
// Little hack to get settings cogs to appear on first load
this._discordUsername = user.discord;
this._telegramUsername = user.telegram;
this._matrixID = user.matrix;
this.discord = user.discord;
this.telegram = user.telegram;
this.matrix = user.matrix;
this.last_active = user.last_active;
this.admin = user.admin;
this.disabled = user.disabled;
this.expiry = user.expiry;
this.notify_discord = user.notify_discord;
this.notify_telegram = user.notify_telegram;
this.notify_matrix = user.notify_matrix;
this.notify_email = user.notify_email;
this.discord_id = user.discord_id;
}

View File

@ -1,4 +1,4 @@
import { _get, _post, toggleLoader } from "../modules/common.js";
import { _get, _post, toggleLoader, addLoader, removeLoader } from "../modules/common.js";
import { Marked } from "@ts-stack/markdown";
import { stripMarkdown } from "../modules/stripmd.js";
@ -666,6 +666,40 @@ export class settingsList {
}
}
private _addMatrix = () => {
// Modify the login modal, why not
const modal = document.getElementById("form-matrix") as HTMLFormElement;
modal.onsubmit = (event: Event) => {
event.preventDefault();
const button = modal.querySelector("span.submit") as HTMLSpanElement;
addLoader(button);
let send = {
homeserver: (document.getElementById("matrix-homeserver") as HTMLInputElement).value,
username: (document.getElementById("matrix-user") as HTMLInputElement).value,
password: (document.getElementById("matrix-password") as HTMLInputElement).value
}
_post("/matrix/login", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
removeLoader(button);
if (req.status == 400) {
window.notifications.customError("errorUnknown", window.lang.notif(req.response["error"] as string));
return;
} else if (req.status == 401) {
window.notifications.customError("errorUnauthorized", req.response["error"] as string);
return;
} else if (req.status == 500) {
window.notifications.customError("errorAddMatrix", window.lang.notif("errorFailureCheckLogs"));
return;
}
window.modals.matrix.close();
_post("/restart", null, () => {});
window.location.reload();
}
}, true);
};
window.modals.matrix.show();
}
reload = () => _get("/config", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
@ -698,6 +732,17 @@ export class settingsList {
icon.onclick = () => window.updater.checkForUpdates(window.modals.updateInfo.show);
}
this.addSection(name, settings.sections[name], icon);
} else if (name == "matrix" && !window.matrixEnabled) {
const addButton = document.createElement("div");
addButton.classList.add("tooltip", "left");
addButton.innerHTML = `
<span class="button ~neutral !normal">+</span>
<span class="content sm">
${window.lang.strings("linkMatrix")}
</span>
`;
(addButton.querySelector("span.button") as HTMLSpanElement).onclick = this._addMatrix;
this.addSection(name, settings.sections[name], addButton);
} else {
this.addSection(name, settings.sections[name]);
}

View File

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

View File

@ -123,6 +123,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"email_enabled": emailEnabled,
"telegram_enabled": telegramEnabled,
"discord_enabled": discordEnabled,
"matrix_enabled": matrixEnabled,
"notifications": notificationsEnabled,
"version": version,
"commit": commit,