diff --git a/api.go b/api.go
index 75a013c..6a4e38f 100644
--- a/api.go
+++ b/api.go
@@ -490,6 +490,9 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
tgUser.Lang = lang
}
+ if app.storage.telegram == nil {
+ app.storage.telegram = map[string]TelegramUser{}
+ }
app.storage.telegram[user.ID] = tgUser
err := app.storage.storeTelegramUsers()
if err != nil {
@@ -1474,6 +1477,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
// @Param lang query string false "Language for email titles."
// @Success 200 {object} emailListDTO
// @Router /config/emails [get]
+// @Security Bearer
// @tags Configuration
func (app *appContext) GetCustomEmails(gc *gin.Context) {
lang := gc.Query("lang")
@@ -1502,6 +1506,7 @@ func (app *appContext) GetCustomEmails(gc *gin.Context) {
// @Failure 500 {object} boolResponse
// @Param id path string true "ID of email"
// @Router /config/emails/{id} [post]
+// @Security Bearer
// @tags Configuration
func (app *appContext) SetCustomEmail(gc *gin.Context) {
var req customEmail
@@ -1561,6 +1566,7 @@ func (app *appContext) SetCustomEmail(gc *gin.Context) {
// @Param enable/disable path string true "enable/disable"
// @Param id path string true "ID of email"
// @Router /config/emails/{id}/state/{enable/disable} [post]
+// @Security Bearer
// @tags Configuration
func (app *appContext) SetCustomEmailState(gc *gin.Context) {
id := gc.Param("id")
@@ -1610,6 +1616,7 @@ func (app *appContext) SetCustomEmailState(gc *gin.Context) {
// @Failure 500 {object} boolResponse
// @Param id path string true "ID of email"
// @Router /config/emails/{id} [get]
+// @Security Bearer
// @tags Configuration
func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
lang := app.storage.lang.chosenEmailLang
@@ -1798,6 +1805,7 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
// @Produce json
// @Success 200 {object} checkUpdateDTO
// @Router /config/update [get]
+// @Security Bearer
// @tags Configuration
func (app *appContext) CheckUpdate(gc *gin.Context) {
if !app.newUpdate {
@@ -1812,6 +1820,7 @@ func (app *appContext) CheckUpdate(gc *gin.Context) {
// @Success 400 {object} stringResponse
// @Success 500 {object} boolResponse
// @Router /config/update [post]
+// @Security Bearer
// @tags Configuration
func (app *appContext) ApplyUpdate(gc *gin.Context) {
if !app.update.CanUpdate {
@@ -1837,6 +1846,7 @@ func (app *appContext) ApplyUpdate(gc *gin.Context) {
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /logout [post]
+// @Security Bearer
// @tags Other
func (app *appContext) Logout(gc *gin.Context) {
cookie, err := gc.Cookie("refresh")
@@ -1910,7 +1920,94 @@ func (app *appContext) ServeLang(gc *gin.Context) {
respondBool(400, false, gc)
}
-// @Summary Returns true/false on whether or not a telegram PIN was verified.
+// @Summary Returns a new Telegram verification PIN, and the bot username.
+// @Produce json
+// @Success 200 {object} telegramPinDTO
+// @Router /telegram/pin [get]
+// @Security Bearer
+// @tags Other
+func (app *appContext) TelegramGetPin(gc *gin.Context) {
+ gc.JSON(200, telegramPinDTO{
+ Token: app.telegram.NewAuthToken(),
+ Username: app.telegram.username,
+ })
+}
+
+// @Summary Link a Jellyfin & Telegram user together via a verification PIN.
+// @Produce json
+// @Param telegramSetDTO body telegramSetDTO true "Token and user's Jellyfin ID."
+// @Success 200 {object} boolResponse
+// @Failure 500 {object} boolResponse
+// @Failure 400 {object} boolResponse
+// @Router /users/telegram [post]
+// @Security Bearer
+// @tags Other
+func (app *appContext) TelegramAddUser(gc *gin.Context) {
+ var req telegramSetDTO
+ gc.BindJSON(&req)
+ if req.Token == "" || req.ID == "" {
+ respondBool(400, false, gc)
+ return
+ }
+ tokenIndex := -1
+ for i, v := range app.telegram.verifiedTokens {
+ if v.Token == req.Token {
+ tokenIndex = i
+ break
+ }
+ }
+ if tokenIndex == -1 {
+ respondBool(500, false, gc)
+ return
+ }
+ tgToken := app.telegram.verifiedTokens[tokenIndex]
+ tgUser := TelegramUser{
+ ChatID: tgToken.ChatID,
+ Username: tgToken.Username,
+ Contact: true,
+ }
+ if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
+ tgUser.Lang = lang
+ }
+ if app.storage.telegram == nil {
+ app.storage.telegram = map[string]TelegramUser{}
+ }
+ app.storage.telegram[req.ID] = tgUser
+ err := app.storage.storeTelegramUsers()
+ if err != nil {
+ app.err.Printf("Failed to store Telegram users: %v", err)
+ } else {
+ app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
+ app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1]
+ }
+ respondBool(200, true, gc)
+}
+
+// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
+// @Produce json
+// @Success 200 {object} boolResponse
+// @Param pin path string true "PIN code to check"
+// @Router /telegram/verified/{pin} [get]
+// @Security Bearer
+// @tags Other
+func (app *appContext) TelegramVerified(gc *gin.Context) {
+ pin := gc.Param("pin")
+ tokenIndex := -1
+ for i, v := range app.telegram.verifiedTokens {
+ if v.Token == pin {
+ tokenIndex = i
+ break
+ }
+ }
+ // if tokenIndex != -1 {
+ // length := len(app.telegram.verifiedTokens)
+ // app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1]
+ // app.telegram.verifiedTokens = app.telegram.verifiedTokens[:length-1]
+ // }
+ respondBool(200, tokenIndex != -1, gc)
+}
+
+// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code.
// @Produce json
// @Success 200 {object} boolResponse
// @Success 401 {object} boolResponse
@@ -1918,7 +2015,7 @@ func (app *appContext) ServeLang(gc *gin.Context) {
// @Param invCode path string true "invite Code"
// @Router /invite/{invCode}/telegram/verified/{pin} [get]
// @tags Other
-func (app *appContext) TelegramVerified(gc *gin.Context) {
+func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok {
respondBool(401, false, gc)
@@ -1942,6 +2039,7 @@ func (app *appContext) TelegramVerified(gc *gin.Context) {
// @Summary Restarts the program. No response means success.
// @Router /restart [post]
+// @Security Bearer
// @tags Other
func (app *appContext) restart(gc *gin.Context) {
app.info.Println("Restarting...")
diff --git a/css/base.css b/css/base.css
index 95f1b4b..976062b 100644
--- a/css/base.css
+++ b/css/base.css
@@ -39,6 +39,11 @@
}
}
+.chip.btn:hover:not([disabled]):not(.textarea),
+.chip.btn:focus:not([disabled]):not(.textarea) {
+ filter: brightness(var(--button-filter-brightness,95%));
+}
+
.banner {
margin: calc(-1 * var(--spacing-4,1rem));
}
diff --git a/html/admin.html b/html/admin.html
index 955e1be..a074ec3 100644
--- a/html/admin.html
+++ b/html/admin.html
@@ -309,6 +309,24 @@
{{ .strings.update }}
+ {{ if .telegram_enabled }}
+
+
+
{{ .strings.linkTelegram }}
+
{{ .strings.sendPIN }}
+
+
+
+
+
+
+
+ @
+
+
{{ .strings.success }}
+
+
+ {{ end }}
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json
index dcd9d6a..4082881 100644
--- a/lang/admin/en-us.json
+++ b/lang/admin/en-us.json
@@ -92,7 +92,8 @@
"inviteExpiresInTime": "Expires in {n}",
"notifyEvent": "Notify on:",
"notifyInviteExpiry": "On expiry",
- "notifyUserCreation": "On user creation"
+ "notifyUserCreation": "On user creation",
+ "sendPIN": "Ask the user to send the PIN below to the bot."
},
"notifications": {
"changedEmailAddress": "Changed email address of {n}.",
@@ -104,6 +105,7 @@
"setOmbiDefaults": "Stored ombi defaults.",
"updateApplied": "Update applied, please restart.",
"updateAppliedRefresh": "Update applied, please refresh.",
+ "telegramVerified": "Telegram account verified.",
"errorConnection": "Couldn't connect to jfa-go.",
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
diff --git a/lang/common/en-us.json b/lang/common/en-us.json
index 8dd076a..9d77a89 100644
--- a/lang/common/en-us.json
+++ b/lang/common/en-us.json
@@ -14,6 +14,9 @@
"copied": "Copied",
"time24h": "24h Time",
"time12h": "12h Time",
+ "linkTelegram": "Link Telegram",
+ "contactEmail": "Contact through Email",
+ "contactTelegram": "Contact through Telegram",
"theme": "Theme"
}
}
diff --git a/lang/form/en-us.json b/lang/form/en-us.json
index 36b1089..391d672 100644
--- a/lang/form/en-us.json
+++ b/lang/form/en-us.json
@@ -18,10 +18,7 @@
"confirmationRequired": "Email confirmation required",
"confirmationRequiredMessage": "Please check your email inbox to verify your address.",
"yourAccountIsValidUntil": "Your account will be valid until {date}.",
- "linkTelegram": "Link Telegram",
- "sendPIN": "Send the PIN below to the bot, then come back here to link your account.",
- "contactEmail": "Contact through Email",
- "contactTelegram": "Contact through Telegram"
+ "sendPIN": "Send the PIN below to the bot, then come back here to link your account."
},
"notifications": {
"errorUserExists": "User already exists.",
diff --git a/models.go b/models.go
index 8bb844f..e7adc17 100644
--- a/models.go
+++ b/models.go
@@ -237,3 +237,13 @@ type checkUpdateDTO struct {
New bool `json:"new"` // Whether or not there's a new update.
Update Update `json:"update"`
}
+
+type telegramPinDTO struct {
+ Token string `json:"token" example:"A1-B2-3C"`
+ Username string `json:"username"`
+}
+
+type telegramSetDTO struct {
+ Token string `json:"token" example:"A1-B2-3C"`
+ ID string `json:"id"` // Jellyfin ID of user.
+}
diff --git a/router.go b/router.go
index 63892ad..2d17e26 100644
--- a/router.go
+++ b/router.go
@@ -119,7 +119,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
router.Use(static.Serve(p+"/invite/", app.webFS))
router.GET(p+"/invite/:invCode", app.InviteProxy)
if app.config.Section("telegram").Key("enabled").MustBool(false) {
- router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerified)
+ router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
}
}
if *SWAGGER {
@@ -158,6 +158,11 @@ 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 app.config.Section("telegram").Key("enabled").MustBool(false) {
+ api.GET(p+"/telegram/pin", app.TelegramGetPin)
+ api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
+ api.POST(p+"/users/telegram", app.TelegramAddUser)
+ }
if app.config.Section("ombi").Key("enabled").MustBool(false) {
api.GET(p+"/ombi/users", app.OmbiUsers)
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)
diff --git a/ts/admin.ts b/ts/admin.ts
index a7516bb..6c14be7 100644
--- a/ts/admin.ts
+++ b/ts/admin.ts
@@ -62,6 +62,10 @@ window.availableProfiles = window.availableProfiles || [];
window.modals.extendExpiry = new Modal(document.getElementById("modal-extend-expiry"));
window.modals.updateInfo = new Modal(document.getElementById("modal-update"));
+
+ if (window.telegramEnabled) {
+ window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
+ }
})();
var inviteCreator = new createInvite();
diff --git a/ts/form.ts b/ts/form.ts
index 17c84a1..0cd1611 100644
--- a/ts/form.ts
+++ b/ts/form.ts
@@ -53,7 +53,10 @@ if (window.telegramEnabled) {
toggleLoader(waiting);
window.telegramModal.show();
let modalClosed = false;
- window.telegramModal.onclose = () => { modalClosed = true; }
+ window.telegramModal.onclose = () => {
+ modalClosed = true;
+ toggleLoader(waiting);
+ }
const checkVerified = () => _get("/invite/" + window.code + "/telegram/verified/" + window.telegramPIN, null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 401) {
@@ -63,7 +66,6 @@ if (window.telegramEnabled) {
} else if (req.status == 200) {
if (req.response["success"] as boolean) {
telegramVerified = true;
- toggleLoader(waiting);
waiting.classList.add("~positive");
waiting.classList.remove("~info");
window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]);
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index 60bef54..4201895 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -1,4 +1,4 @@
-import { _get, _post, _delete, toggleLoader, toDateString } from "../modules/common.js";
+import { _get, _post, _delete, toggleLoader, addLoader, removeLoader, toDateString } from "../modules/common.js";
import { templateEmail } from "../modules/settings.js";
import { Marked } from "@ts-stack/markdown";
import { stripMarkdown } from "../modules/stripmd.js";
@@ -14,6 +14,11 @@ interface User {
telegram: string;
}
+interface getPinResponse {
+ token: string;
+ username: string;
+}
+
class user implements User {
private _row: HTMLTableRowElement;
private _check: HTMLInputElement;
@@ -80,7 +85,8 @@ class user implements User {
if (!window.telegramEnabled) return;
this._telegramUsername = u;
if (u == "") {
- this._telegram.textContent = "";
+ this._telegram.innerHTML = `Add`;
+ (this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
} else {
this._telegram.innerHTML = `@${u}`;
}
@@ -193,6 +199,50 @@ class user implements User {
});
}
+ private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => {
+ if (req.readyState == 4 && req.status == 200) {
+ const pin = document.getElementById("telegram-pin");
+ const link = document.getElementById("telegram-link") as HTMLAnchorElement;
+ const username = document.getElementById("telegram-username") as HTMLSpanElement;
+ const waiting = document.getElementById("telegram-waiting") as HTMLSpanElement;
+ let resp = req.response as getPinResponse;
+ pin.textContent = resp.token;
+ link.href = "https://t.me/" + resp.username;
+ username.textContent = resp.username;
+ addLoader(waiting);
+ let modalClosed = false;
+ window.modals.telegram.onclose = () => {
+ modalClosed = true;
+ removeLoader(waiting);
+ }
+ let send = {
+ token: resp.token,
+ id: this.id
+ };
+ const checkVerified = () => _post("/users/telegram", send, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ if (req.status == 200 && req.response["success"] as boolean) {
+ removeLoader(waiting);
+ waiting.classList.add("~positive");
+ waiting.classList.remove("~info");
+ window.notifications.customSuccess("telegramVerified", window.lang.notif("telegramVerified"));
+ setTimeout(() => {
+ window.modals.telegram.close();
+ waiting.classList.add("~info");
+ waiting.classList.remove("~positive");
+ }, 2000);
+ document.dispatchEvent(new CustomEvent("accounts-reload"));
+ } else if (!modalClosed) {
+ setTimeout(checkVerified, 1500);
+ }
+ }
+ }, true);
+ window.modals.telegram.show();
+ checkVerified();
+ }
+ });
+
+
update = (user: User) => {
this.id = user.id;
this.name = user.name;
@@ -723,6 +773,7 @@ export class accountsList {
this._selectAll.onchange = () => {
this.selectAll = this._selectAll.checked;
};
+ document.addEventListener("accounts-reload", this.reload);
document.addEventListener("accountCheckEvent", () => { this._checkCount++; this._checkCheckCount(); });
document.addEventListener("accountUncheckEvent", () => { this._checkCount--; this._checkCheckCount(); });
this._addUserButton.onclick = window.modals.addUser.toggle;
diff --git a/ts/modules/common.ts b/ts/modules/common.ts
index 4142f42..f761a4c 100644
--- a/ts/modules/common.ts
+++ b/ts/modules/common.ts
@@ -179,3 +179,22 @@ export function toggleLoader(el: HTMLElement, small: boolean = true) {
el.appendChild(dot);
}
}
+
+export function addLoader(el: HTMLElement, small: boolean = true) {
+ if (!el.classList.contains("loader")) {
+ el.classList.add("loader");
+ if (small) { el.classList.add("loader-sm"); }
+ const dot = document.createElement("span") as HTMLSpanElement;
+ dot.classList.add("dot")
+ el.appendChild(dot);
+ }
+}
+
+export function removeLoader(el: HTMLElement, small: boolean = true) {
+ if (el.classList.contains("loader")) {
+ el.classList.remove("loader");
+ el.classList.remove("loader-sm");
+ const dot = el.querySelector("span.dot");
+ if (dot) { dot.remove(); }
+ }
+}
diff --git a/ts/typings/d.ts b/ts/typings/d.ts
index 70a260f..7e0bf3e 100644
--- a/ts/typings/d.ts
+++ b/ts/typings/d.ts
@@ -4,6 +4,8 @@ declare interface Modal {
show: () => void;
close: (event?: Event) => void;
toggle: () => void;
+ onopen: (f: () => void) => void;
+ onclose: (f: () => void) => void;
}
interface ArrayConstructor {
@@ -98,6 +100,7 @@ declare interface Modals {
customizeEmails: Modal;
extendExpiry: Modal;
updateInfo: Modal;
+ telegram: Modal;
}
interface Invite {