Compare commits

...

134 Commits

Author SHA1 Message Date
Harvey Tindall
c10f1e3b36 list/search/accounts: fully(?) page-based search
searches are triggered by setting the search QP, and ran by a listener
set on PageManager. same with details. Works surprisingly well, but i'm
sure theres bugs.
2025-12-25 13:04:03 +00:00
Harvey Tindall
3308739619 admin: tab system improvement, search: ?search qp
tab content classes (e.g. settingsList, activityList)
can implement "AsTab", "Navigatable" and or "PageEventBindable",
the first giving them a tab name, a subpath and a reloader function,
the second an "isURL" and "navigate" function for loading resources,
the last giving them bind/unbindPageEvent methods. These are looped
through in ts/admin.ts still crudely, maybe tabs.ts could accept "AsTab"
implementers directly.

"Search" class now has a ?search query param which just encodes the
search box content, set when you perform a server search (hit enter or
press the button). ?user queries from the accounts or activity tab will
be converted to this form on loading.
2025-12-24 12:56:02 +00:00
Harvey Tindall
748acc13c0 accounts: show table row when clicking into details, more usage of
RadioBasedTabs

when clicking a username in the accounts tab, the details comes up with
a copy of the row you clicked, so you can view logs and edit the user.
Also decided not to use a RadioBasedTab here, instead providing a link
to the activity tab with a search for the user (added the ability to do
that, ?user=username). FIXME: handle deleting the user or just generally
editing them.
2025-12-23 20:37:29 +00:00
Harvey Tindall
c3bac6c51c ui: select by index, id over name, buttonHTML on RadioBasedTabSelector
pass a string (new "id" field) or number to RadioBasedTabSelector.selected = x.
setting an "id" for a tab is optional, if not set it's value defaults to the "name" value.
Also added optional buttonHTML to put inside the button insetad of the
given name.
2025-12-23 15:12:44 +00:00
Harvey Tindall
3e39657642 ui: add RadioBasedTabs
does the "tabs" seen on the invite tab (invite expiry/user expiry), and
will be used in all other similar cases.
2025-12-21 19:55:49 +00:00
Harvey Tindall
41334e9051 api-users: add individual user summary route
GET /users/:id, skips the usercache.
2025-12-21 19:19:46 +00:00
Harvey Tindall
e2c34e574d jf_activity: ui changes
just committing so I can pull on another device.
2025-12-21 19:07:03 +00:00
Harvey Tindall
b6459b665d jf_activity: paginated list in ui
added POST route for pagination to activity route, a count route, and
modified Search and PaginatedList a bit to support lists without search
fields (essentially just running an empty search). Visible by clicking
on a user's name in the accounts tab.
2025-12-20 18:27:39 +00:00
Harvey Tindall
d72a5c91cf jf_activity_test: reduce time to run
values were reasonable when testing with a size limit of 20 or
something, now it's ~6000.
2025-12-20 11:58:56 +00:00
Harvey Tindall
cacd992aad jf_activity: remove debug line
also bump mediabrowser again to do the same.
2025-12-20 11:49:55 +00:00
Harvey Tindall
23ad016476 mediabrowser: bump to v0.3.34
forgot to switch back from my local copy in the previous commit.
2025-12-20 11:41:35 +00:00
Harvey Tindall
cc571409b9 Merge branch 'jf-log'
accidentally did a fix on the wrong branch, so i'm merging it
prematurely.
2025-12-20 11:23:18 +00:00
Harvey Tindall
d7bad69d40 jf-actvitity: functioning route, ombi fixes
forgot to switch branches before doing a fix for #455, so it's in here
too. OmbiUserByJfID/getOmbiUser takes an optional email *string, to
optionally pass an override email address to search with, used when
changing it.
2025-12-20 11:21:13 +00:00
Harvey Tindall
63bb678a72 Merge branch 'main' of github.com:hrfee/jfa-go 2025-12-19 13:12:27 +00:00
Harvey Tindall
2a41c5a393 admin: scroll current tab in nav to center
for #456, ensures you can see and press both the next and previous tabs
    even if you can't scroll for some reason.
2025-12-19 13:11:37 +00:00
Harvey Tindall
9b80492b4f userpage: fix display of email confirmation required message
recent adjustments to modals and CSS in general meant a "content" class
was removed that was being querySelector'ed. For #455.
2025-12-16 18:37:54 +00:00
Harvey Tindall
315d74eb2d userpage: fix redirect on contact method change
root userpage paths ("") would mess up the redirect, wrapper function
returns "/" if thats the case. For #455.
2025-12-16 18:32:37 +00:00
Harvey Tindall
c21df253a1 jf-activity: initial changes
functionality mostly there but needs a UI.
2025-12-16 18:15:39 +00:00
Harvey Tindall
e4e9369d54 theme: set theme-color for ios devices
colours the search bar, sometimes.
2025-12-11 14:47:22 +00:00
Harvey Tindall
26d05911fd ui: fix hidden overflow on mobile devices
mostly the accounts table was causing an overflow on mobile browsers,
specifically anything on ios and samsung browser. For #450.
2025-12-10 20:37:29 +00:00
Harvey Tindall
85a6d66228 html: fix headers
go's html/template was thinking header.html and other template snippets
were full documents, so inserting their own <html> and other tags,
breaking the head up. Renaming to txt seems to fix things.
2025-12-10 20:31:45 +00:00
Harvey Tindall
1c755306f9 tray: fix broken dependency. 2025-12-10 17:43:00 +00:00
Harvey Tindall
2e97142d9e accounts: reduce initial load time even further
took the referralCache idea further and did it for all db queries. Now
take ~40ms for ~5000 users on arm64 through QEMU, and ~60ms on a
rockpro64.
2025-12-10 17:39:54 +00:00
Harvey Tindall
a08a0fd3e6 accounts: reduce initial load time further
When generating the cache and calling userSummary per user, previously
the DB was queried for an invite with the ReferrerJellyfinID set to the
user's ID. This was fast enough on my test system (~5000 users in
~1.5s), while testing cross-compilation, I found it ran extremely slow
on an arm64 build (running through QEMU admittedly), doing ~5000 in
~18s. Instead, a map of IDs to invites/referrals is generated once and
queried instead. Initial load now takes ~80ms on my system, and 0.95s
through QEMU, and 0.68s on a rockpro64 SBC.
2025-12-10 16:44:21 +00:00
Harvey Tindall
420a22970d goreleaser: unset GOOS/GOARCH for precompile step
these should be run natively.
2025-12-10 15:05:30 +00:00
Harvey Tindall
d4109c8cf5 matrix: use goolm over libolm, ci: user smaller docker image base
removes one dependency. jfa-go-build-docker has been updated to reflect
this, and in general with a newer debian version and properly included
goreleaser, and a build for amd64. Dockerfiles now use a
"distroless"-style container as their base. Was gonna use
chainguard/glibc-dynamic, but it running as a different user meant it
wouldn't read/write from your /data mount without manual intervention.
2025-12-10 13:25:21 +00:00
Harvey Tindall
945d579f57 config: un-advanced date format, fix dependencies
changed dependency on "method" (from when it was in the email section)
to "enabled". For #453.
2025-12-10 09:51:12 +00:00
Harvey Tindall
6e1f07563d ci: use caches for container build too 2025-12-09 17:53:26 +00:00
Harvey Tindall
ce6f8b41dc ci: use npm cache too 2025-12-09 17:43:04 +00:00
Harvey Tindall
aa75305da4 scripts/version.sh: downgrade version
use go 1.24 and remove usage of wg.Go().
2025-12-09 17:42:45 +00:00
Harvey Tindall
03cf533c9d ci: use podman volumes for go mod/build cache 2025-12-09 17:38:12 +00:00
Harvey Tindall
ccf0584db8 build: use tsgo for type checking
I don't use and fancy typescript features, and apparently this version
is very usable at this point. Removes the typescript dependency, which
should have been in dev-deps anyway.
2025-12-09 17:31:23 +00:00
Harvey Tindall
833fd26091 scripts/variants: add parallel flag
doesn't result in much gain on my machine or my CI server so it's not
the default. On local machine (5800x), went from ~5ms to ~4ms, on my CI
(hetzner ampere altra with 8 vCPUs) went from ~15ms to ~10ms.
2025-12-09 15:44:47 +00:00
Harvey Tindall
fd72c838c3 userpage: don't trigger submit on "reset password" press
Fiex #452.
2025-12-09 15:25:54 +00:00
Harvey Tindall
63c770db73 css: fix lists with RTL
changes text-align:left to start, and margin-left: to
margin-inline-start:. Submitted a PR to a17t to change these values as
well. For #450.
2025-12-09 15:06:49 +00:00
Harvey Tindall
6237620390 scripts/variants: add verbose -v flag
disable output by default, enable with -v
2025-12-09 14:38:48 +00:00
Harvey Tindall
4a9bac1027 scripts: replace dark-variant.sh with go scripts/variants
uses regex mostly, resulting in a significantly faster execution (old:
~2s, new: ~31ms). Only matches classList.add/remove and class="..."
(won't touch string literal variables or colours), but these weren't
really used anyway. Supports multi-line classList.add/remove, which was
the reason I wrote this anyway (formatting the codebase introduced some
of these).
2025-12-09 14:28:05 +00:00
Harvey Tindall
817107622a ts: format finally
formatted with biome, a config file is provided.
2025-12-08 20:38:30 +00:00
Harvey Tindall
ca7c553147 telegram: add "always use default lang", /lang default
For #451. Setting in integrations > chat bots > telegram makes jfa-go
ignore the telegram users clients lanugage setting and always use
whatever is set as the default language in jfa-go. Also added /lang
default, to un-set a preferred language. the start message also now
shows up on typing /help or !help too.
2025-12-08 18:16:27 +00:00
Harvey Tindall
d00507fd20 accounts: fix matrix input
was too small to see what you were typing.
2025-12-08 17:50:50 +00:00
Harvey Tindall
2bbf97be19 accounts/activity: fix clear search button on RTL
use margin-inline-start instead of margin-left. For #450.
2025-12-08 17:46:11 +00:00
Harvey Tindall
4f135220bc lang: display ckb (kurdish) as RTL 2025-12-08 15:36:03 +00:00
Harvey Tindall
a2500add5a lang: add lang tag to name of Farsi and Kurdish 2025-12-08 15:35:12 +00:00
Harvey Tindall
26284e89f6 web: add more RTL fixes
For #450.
2025-12-08 15:30:24 +00:00
KSAm3lm
8030a7c896 Translated using Weblate (Arabic)
Currently translated at 100.0% (136 of 136 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/ar/
2025-12-08 16:13:58 +01:00
KSAm3lm
5010159621 Translated using Weblate (Arabic)
Currently translated at 100.0% (59 of 59 strings)

Translation: jfa-go/Emails
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/emails/ar/
2025-12-08 16:13:58 +01:00
KSAm3lm
92090bcf99 translation from Weblate (Arabic)
Currently translated at 100.0% (304 of 304 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/ar/
2025-12-08 16:13:58 +01:00
KSAm3lm
655096ea1e Translated using Weblate (Arabic)
Currently translated at 100.0% (56 of 56 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/common-strings/ar/
2025-12-08 16:13:58 +01:00
Harvey Tindall
362984a391 web: remove almost every use of ml/mr
replace with flex and gap mostly. For #450.
2025-12-08 15:12:40 +00:00
Harvey Tindall
9c9e55147d lang: don't show lanugages with empty name fields 2025-12-08 14:38:24 +00:00
Harvey Tindall
f7a72133c6 lang: lowercase names 2025-12-08 14:36:01 +00:00
Harvey Tindall
341dd5930e settings: force LTR always
since it's untranslatable for now.
2025-12-07 20:43:14 +00:00
Harvey Tindall
5baa3b98cd tooltip: add .force-ltr class 2025-12-07 20:43:01 +00:00
Harvey Tindall
99c6559a54 ui: set RTL on some languages
checking with a list I found online. need to set some things as forced
ltr, such as settings. For #450.
2025-12-07 20:38:43 +00:00
Harvey Tindall
12827f6c84 admin: remove all non-dynamic ml/mr usage
replaced with flex gaps. For #450.
2025-12-07 20:28:05 +00:00
Harvey Tindall
ad942fd194 tooltip: flip on rtl 2025-12-07 19:50:15 +00:00
Harvey Tindall
b5e348ad2b ui: correct html lang tag
set according to the language picked.
2025-12-07 19:31:52 +00:00
KSAm3lm
0b8376a19e translation from Weblate (Arabic)
Currently translated at 48.6% (139 of 286 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
b64e2ac9a5 Translated using Weblate (Arabic)
Currently translated at 100.0% (136 of 136 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
c9e2e72d60 translation from Weblate (Arabic)
Currently translated at 40.5% (116 of 286 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
fc990a9ac2 translation from Weblate (Arabic)
Currently translated at 34.9% (100 of 286 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
8601fbf8e0 translation from Weblate (Arabic)
Currently translated at 100.0% (68 of 68 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
48b547661f translation from Weblate (Arabic)
Currently translated at 26.9% (77 of 286 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
423b060857 Translated using Weblate (Arabic)
Currently translated at 100.0% (136 of 136 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
48cf1a7e39 translation from Weblate (Arabic)
Currently translated at 26.9% (77 of 286 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
47954db751 Translated using Weblate (Arabic)
Currently translated at 100.0% (54 of 54 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/common-strings/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
1fe868b41e translation from Weblate (Arabic)
Currently translated at 100.0% (68 of 68 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
07f683282a Translated using Weblate (Arabic)
Currently translated at 61.0% (83 of 136 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
0a98eea1ac translation from Weblate (Arabic)
Currently translated at 26.9% (77 of 286 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
40d86214cb Translated using Weblate (Arabic)
Currently translated at 59.5% (81 of 136 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
959b10c2a1 Translated using Weblate (Arabic)
Currently translated at 100.0% (54 of 54 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/common-strings/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
f11fc2e3bc Translated using Weblate (Arabic)
Currently translated at 13.2% (18 of 136 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
2e2fcb0beb translation from Weblate (Arabic)
Currently translated at 26.2% (75 of 286 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
d2d90083be Translated using Weblate (Arabic)
Currently translated at 100.0% (54 of 54 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/common-strings/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
355ffd5975 translation from Weblate (Arabic)
Currently translated at 100.0% (68 of 68 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
0010ece89c Translated using Weblate (Arabic)
Currently translated at 100.0% (13 of 13 strings)

Translation: jfa-go/Telegram/Matrix/Discord bots
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/chat-bots/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
c8db639701 Translated using Weblate (Arabic)
Currently translated at 11.7% (16 of 136 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
3bb499dc9d Translated using Weblate (Arabic)
Currently translated at 100.0% (59 of 59 strings)

Translation: jfa-go/Emails
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/emails/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
45d83435cb translation from Weblate (Arabic)
Currently translated at 25.8% (74 of 286 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
2535aba2fe Translated using Weblate (Arabic)
Currently translated at 100.0% (54 of 54 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/common-strings/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
3dc039135a Translated using Weblate (Arabic)
Currently translated at 100.0% (10 of 10 strings)

Translation: jfa-go/Password Reset Links
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/password-reset-links/ar/
2025-12-07 20:07:14 +01:00
KSAm3lm
726f90bfae Translated using Weblate (Arabic)
Currently translated at 100.0% (13 of 13 strings)

Translation: jfa-go/Telegram/Matrix/Discord bots
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/chat-bots/ar/
2025-12-07 20:07:14 +01:00
Harvey Tindall
709c394d2d invites: use string getting func on CopyButton
should fix #449.
2025-12-07 11:40:16 +00:00
Harvey Tindall
fb5ea43ef5 CopyButton: add string getting func option 2025-12-07 11:38:46 +00:00
Harvey Tindall
49f2026c47 npm: bump esbuild 2025-12-06 20:25:06 +00:00
Harvey Tindall
5ad225fa78 Merge pull request #395 from hrfee/dependabot/npm_and_yarn/cross-spawn-7.0.6
build(deps): bump cross-spawn from 7.0.3 to 7.0.6
2025-12-06 20:21:25 +00:00
Harvey Tindall
155edc997a Merge branch 'main' into dependabot/npm_and_yarn/cross-spawn-7.0.6 2025-12-06 20:20:34 +00:00
Harvey Tindall
591547c167 Merge branch 'main' of github.com:hrfee/jfa-go 2025-12-06 20:19:13 +00:00
Harvey Tindall
14e8e63f30 easyproxy: update deps
closes #398.
2025-12-06 20:18:50 +00:00
Harvey Tindall
ccba0e5137 Merge pull request #434 from hrfee/dependabot/npm_and_yarn/glob-10.5.0
build(deps): bump glob from 10.4.5 to 10.5.0
2025-12-06 20:16:50 +00:00
Harvey Tindall
a62e6a5dfc Merge pull request #435 from hrfee/dependabot/npm_and_yarn/site/glob-10.5.0
build(deps): bump glob from 10.4.5 to 10.5.0 in /site
2025-12-06 20:16:37 +00:00
Harvey Tindall
975c47a79b settings: fix showing advanced ones
fixes #448.
2025-12-06 20:14:44 +00:00
Harvey Tindall
fcfd5f4981 query unescape more routes
fixes #447. Went through all routes with components in the path to check
if they needed escaping, quite a few did.
2025-12-06 20:04:30 +00:00
Harvey Tindall
e5315095be invites: improve dropdown animation
involves negative margins and removing padding resulting in a messy
expand function, but it looks smooth. Could also be done on the settings
page, but it looks mostly alright there already.
2025-12-06 17:43:54 +00:00
Harvey Tindall
44e4b5fce2 invites: editable label, /invites/edit route
PATCH /invite/edit lets you edit an invite by giving new values for a
subset of inviteDTO (EditableInviteDTO). Replaces /invite/profile and
/invite/notify, and allows changing (user)label and user expiry as well
as the previously customizable values through other routes. An edit
button next to the code/label allows changing on the invites tab.
2025-12-06 15:38:06 +00:00
Harvey Tindall
4bb116417e messages: fix black page background when editing
changed the "body, .body {" to ".body" in the mail CSS so it doesn't
affect the web page. Seems to look fine still in gmail in both light and
dark modes. Fixes #446
2025-12-06 14:05:12 +00:00
Harvey Tindall
51f604d061 ivnites: use actual inviteDTO for DOMInvite
no intermediary parsing step. Also, moved the date -> duration (3mo6d3h
sorta thing) to the web, there's now a ValidTill field with a unix
timestamp. Used the new Temporal api with a polyfill. Bumped api version
(although it still isn't semver).
2025-12-06 14:01:06 +00:00
Harvey Tindall
ab7694b50b messages: improve editor CSS
improved wrapping behaviour.
2025-12-05 20:46:42 +00:00
Harvey Tindall
556e1411f4 invites: add "send to" to dropdown
allows sending to more people after creating the invite. Fixes #444.
2025-12-05 19:51:42 +00:00
Harvey Tindall
5fa528fd2d invites: add /invites/send, store more details in new SentTo field
deprecated SendTo string field for SentTo, which holds successful send
addresses, and failures with a reason that isn't plain text. Will soon
add an interface for sending invites after their creation. For #444
(ha).
2025-12-05 12:03:21 +00:00
Harvey Tindall
3635a13682 fix opening create profile when jellyseerr disabled
fixes #445.
2025-12-04 18:19:33 +00:00
Harvey Tindall
b59cd73e43 messages: switch to new template package
rewrote and put in its own repo. Now supports {if a ==/!= b}, string
literals ({if a == "b"}), else/else if and nested if statements.
2025-12-04 11:50:14 +00:00
Harvey Tindall
6e31a7e2dd form: add pre-signup card
same as the post-signup card, but shown on the sidebar.
2025-12-02 14:56:15 +00:00
Harvey Tindall
a4b94b4f45 tray: switch to fork, use higher res icon
switched to a fork of a fork of getlantern/systray which has no
ayatana-appindicator dependency.
2025-12-02 13:05:06 +00:00
Harvey Tindall
622de21dcf ci: move buildrone upload back to before container build 2025-12-02 11:46:37 +00:00
Harvey Tindall
0ab5bd1705 Merge branch 'main' of github.com:hrfee/jfa-go 2025-12-02 00:41:23 +00:00
Harvey Tindall
f74e85662a email: add migration for pwr email file rename
sorry this was broken for a while for old users.
2025-12-02 00:40:48 +00:00
Harvey Tindall
862217a627 ci: use python3, not python 2025-12-01 17:37:28 +00:00
Harvey Tindall
861e72b331 ci: finalize merged ci files
for merging. renamed to unstable and stable, removed the -docker files,
    and modified the stable one too.
2025-12-01 16:31:13 +00:00
Harvey Tindall
550cb36bd1 ci: fix goreleaser, rename arm_v6, upload to dockerhub temporarily 2025-12-01 15:14:06 +00:00
Harvey Tindall
03d3cee18b ci: attempt to combine git-docker with git-binary
using a second Dockerfile, and the prevous Makefile changes to share the
precompile step. git-binary.yaml temporarily set for ci-streamline
branch and is in a "dry run" form.
2025-12-01 14:52:10 +00:00
Harvey Tindall
331e7c13fa Makefile: share $(DATA) on internal/external, keep record of build flags
This means you could make precompile, then make INTERNAL=on then make
INTERNAL=off and each time it would re-generate the go files only since
the flags have changed.
2025-12-01 14:18:10 +00:00
Harvey Tindall
429538688c ci: pull tags for docker build
needed for CSSVERSION.
2025-12-01 13:24:28 +00:00
Harvey Tindall
70ee98f9f0 fix printf types
was causing builds to fail on ci.
2025-12-01 12:55:24 +00:00
Harvey Tindall
320e9cd9d0 mediabrowser: bump
imrpvoed UserNotFound error classification, no longer vulnerable to
random 404s from proxies or whatever (for Jellyfin only, not emby)
2025-12-01 12:43:40 +00:00
Harvey Tindall
50455b828d housekeeping/discord: respect remove role on disable setting
only remove it if it's set to true.
2025-11-30 21:59:46 +00:00
Harvey Tindall
b525b03ef8 Merge branch 'main' of github.com:hrfee/jfa-go 2025-11-30 21:42:12 +00:00
Harvey Tindall
641669873d setup: mark external URL required
this should really be given but things'll probably work without it.
2025-11-30 21:40:59 +00:00
Harvey Tindall
fb1b673dee settings: add "tasks" button (advanced)
added a GET /tasks route to list tasks with a description (untranslated,
but this is mostly a dev feature anyway). Loaded in a modal by enabling
advanced settings and pressing the Tasks button at the top (where logs,
backups, restart are). Also added some icons in settings, and removed
some redundant "flex flex-row"s on buttons and reduced the spacing in
those with icons to gap-1.
2025-11-29 15:43:06 +00:00
Harvey Tindall
598a389e3d jellyseerr: fix extremely long import, run only once
cache was being invalidated for every user, and on my 5000 user test
instance, this sweated jellyseerr and my computer (audibly). Also, since
this only needs to realistically run once, a flag is set in the database
to indicate it's been done, and unset once the feature is disabled.
It'll only run on boot if the flag is unset, or if triggered by the
/tasks route. Will likely add manual trigger buttons on the web as well.
2025-11-29 14:13:34 +00:00
Harvey Tindall
1a0e32504f accounts: hide js/ombi checks when not enabled correctly on modify
settings
2025-11-28 20:52:37 +00:00
Harvey Tindall
cbff3085fa profiles: "add jellyseerr" checkbox on profile creation
makes things clearer to new users. Fixes #438.
2025-11-28 20:37:45 +00:00
Harvey Tindall
0ecacc6064 tasks: add /tasks/jellyseerr, document
now live in tasks.go and have actual API documentation.
/tasks/jellyseerr triggers account import.
2025-11-28 17:26:27 +00:00
Harvey Tindall
f36a32773a jellyseerr: fix background daemon auto import of telegram
ChatID fix was done in the TPS implementation but not in the daemon.
2025-11-28 17:10:38 +00:00
Harvey Tindall
58a3fe1f72 jellyseerr: don't attempt to apply profile if disabled
fixes #441
2025-11-28 16:44:03 +00:00
Harvey Tindall
4d58fc5f88 telegram: fix linking on sign-up
fixes #440.
2025-11-28 16:34:39 +00:00
Harvey Tindall
7d947015d3 pwr: allow jellyfin pwrs when not using email
switched if !emailEnabled { return } to if !messagesEnabled { return }.
Fixes #439.
2025-11-28 16:21:00 +00:00
Harvey Tindall
77d2ad3b6b profiles: add ability to directly edit profile JSON
allows for customizing small things, like changing admin status.
2025-11-28 15:13:46 +00:00
Harvey Tindall
f83695190d build: use most recent tag as cssversion
the value is fixed in html files though, hopefully this will the light
the fire under me to make sure I keep changing the version with updates.
2025-11-28 12:36:49 +00:00
Harvey Tindall
815721adb2 profiles: add routes for viewing/modifying
most of Profile struct is now in ProfileDTO (which is embedded in the
former), admin can pull it from /profiles/raw/{name} GET, and replace it
with PUT. Will make a ui for this.
2025-11-28 12:06:17 +00:00
Harvey Tindall
836974e1b2 scripts: stop using gopkg.in/yaml
used a different library in scripts/yaml, forgot to in scripts/ini and
main.go
2025-11-27 20:38:16 +00:00
dependabot[bot]
34f5455ba5 build(deps): bump glob from 10.4.5 to 10.5.0 in /site
Bumps [glob](https://github.com/isaacs/node-glob) from 10.4.5 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 17:42:14 +00:00
dependabot[bot]
1caf9b3c88 build(deps): bump glob from 10.4.5 to 10.5.0
Bumps [glob](https://github.com/isaacs/node-glob) from 10.4.5 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 02:33:43 +00:00
dependabot[bot]
763d231e64 build(deps): bump cross-spawn from 7.0.3 to 7.0.6
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-15 14:52:27 +00:00
150 changed files with 9353 additions and 5216 deletions

1
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1 @@
817107622a8fe6f2fdaf198da4b2632854aa9bac

3
.gitignore vendored
View File

@@ -27,3 +27,6 @@ scripts/langmover/out
tinyproxy.conf
static/banner.svg
start.sh
ts/*.tsbuildinfo
ts/**/*.tsbuildinfo
js/

View File

@@ -8,8 +8,8 @@ release:
name_template: "v{{.Version}}"
before:
hooks:
- npm i
- make precompile
- npm ci
- env GOOS= GOARCH= make precompile
builds:
- id: notray
dir: ./
@@ -34,7 +34,7 @@ builds:
- PKG_CONFIG_PATH=/usr/lib/{{ if eq .Arch "amd64" }}x86_64{{ else if eq .Arch "arm64" }}aarch64{{ else }}{{ .Arch }}{{ end }}-linux-gnu{{ if eq .Arch "arm" }}eabihf{{ end }}/pkgconfig:$PKG_CONFIG_PATH
- GOARM={{ if eq .Arch "arm" }}7{{ end }}
flags:
- -tags=e2ee,{{ .Env.JFA_GO_TAG }}
- -tags=e2ee,goolm,{{ .Env.JFA_GO_TAG }}
ldflags:
- -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater={{.Env.JFA_GO_UPDATER}} {{.Env.JFA_GO_STRIP}} -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
goos:
@@ -65,7 +65,7 @@ builds:
- CXX=x86_64-linux-gnu-gcc
- PKG_CONFIG_PATH=/usr/lib/{{ if eq .Arch "amd64" }}x86_64{{ else if eq .Arch "arm64" }}aarch64{{ else }}{{ .Arch }}{{ end }}-linux-gnu{{ if eq .Arch "arm" }}eabihf{{ end }}/pkgconfig:$PKG_CONFIG_PATH
flags:
- -tags=tray,e2ee,{{ .Env.JFA_GO_TAG }}
- -tags=tray,e2ee,goolm,{{ .Env.JFA_GO_TAG }}
ldflags:
- -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater={{.Env.JFA_GO_UPDATER}} {{.Env.JFA_GO_STRIP}} -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
goos:
@@ -177,13 +177,10 @@ nfpms:
replaces:
- jfa-go
dependencies:
- libayatana-appindicator
- libolm-dev
rpm:
dependencies:
- libappindicator-gtk3
- libolm
apk:
dependencies:
- libayatana-appindicator
- olm

View File

@@ -1,29 +0,0 @@
when:
- event: push
branch: main
steps:
- name: build
image: docker.io/woodpeckerci/plugin-docker-buildx
settings:
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_TOKEN
repo: docker.io/hrfee/jfa-go
tags: unstable
registry: docker.io
platforms: linux/amd64,linux/arm64,linux/arm/v7
build_args:
- BUILT_BY:
from_secret: BUILT_BY
- name: buildrone
image: docker.io/python
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
commands:
- wget https://builds.hrfee.pw/upload.py
- pip install requests
- python upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-unstable=true

View File

@@ -1,29 +0,0 @@
when:
- event: tag
branch: main
steps:
- name: build
image: docker.io/woodpeckerci/plugin-docker-buildx
settings:
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_TOKEN
repo: docker.io/hrfee/jfa-go
tags: latest
registry: docker.io
platforms: linux/amd64,linux/arm64,linux/arm/v7
build_args:
- BUILT_BY:
from_secret: BUILT_BY
- name: buildrone
image: docker.io/python
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
commands:
- wget https://builds.hrfee.pw/upload.py
- pip install requests
- python upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-stable=true

View File

@@ -17,6 +17,10 @@ steps:
JFA_GO_SNAPSHOT: y
JFA_GO_BUILT_BY:
from_secret: BUILT_BY
volumes:
- jfa-go-build-cache:/root/.cache/go-build
- jfa-go-mod-cache:/go/pkg/mod
- jfa-go-npm-cache:/npm
commands:
- npm i
- make precompile
@@ -27,6 +31,10 @@ steps:
JFA_GO_SNAPSHOT: y
JFA_GO_BUILT_BY:
from_secret: BUILT_BY
volumes:
- jfa-go-build-cache:/root/.cache/go-build
- jfa-go-mod-cache:/go/pkg/mod
- jfa-go-npm-cache:/npm
commands:
- make test
- name: build
@@ -36,10 +44,12 @@ steps:
from_secret: BUILT_BY
GITHUB_TOKEN:
from_secret: GITHUB_TOKEN
volumes:
- jfa-go-build-cache:/root/.cache/go-build
- jfa-go-mod-cache:/go/pkg/mod
- jfa-go-npm-cache:/npm
commands:
- curl -sfL https://goreleaser.com/static/run > ../goreleaser
- chmod +x ../goreleaser
- ./scripts/version.sh ../goreleaser
- ./scripts/version.sh goreleaser
- name: deb-repo
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
@@ -51,6 +61,35 @@ steps:
- bash -c 'ssh -i /tmp/id_repo root@apt.hrfee.dev -p 2022 "repo-process-deb trusty"'
- bash -c 'ssh -i /tmp/id_repo root@apt.hrfee.dev -p 2022 "repo-process-deb trusty-unstable"'
- bash -c 'ssh -i /tmp/id_repo root@apt.hrfee.dev -p 2022 "rm -f /repo/incoming/*.deb"'
- name: build-external
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
JFA_GO_BUILT_BY:
from_secret: BUILT_BY
volumes:
- jfa-go-build-cache:/root/.cache/go-build
- jfa-go-mod-cache:/go/pkg/mod
- jfa-go-npm-cache:/npm
commands:
- sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' ./build/data/html/setup.html
- env GOOS=linux INTERNAL=off ./scripts/version.sh goreleaser build --id notray-e2ee --clean
- mv ./dist/notray-e2ee_linux_arm_6 ./dist/notray-e2ee_linux_arm
- name: container
image: docker.io/woodpeckerci/plugin-docker-buildx
settings:
dry_run: false
dockerfile: Dockerfile.ci
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_TOKEN
repo: docker.io/hrfee/jfa-go
tags: stable
registry: docker.io
platforms: linux/amd64,linux/arm64,linux/arm/v7
build_args:
- BUILT_BY:
from_secret: BUILT_BY
- name: buildrone
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
@@ -59,3 +98,4 @@ steps:
commands:
- wget https://builds.hrfee.pw/upload.py
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true'
- python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-stable=true

View File

@@ -18,8 +18,12 @@ steps:
JFA_GO_SNAPSHOT: y
JFA_GO_BUILT_BY:
from_secret: BUILT_BY
volumes:
- jfa-go-build-cache:/root/.cache/go-build
- jfa-go-mod-cache:/go/pkg/mod
- jfa-go-npm-cache:/npm
commands:
- npm i
- npm ci --cache /npm --prefer-offline
- make precompile
- go mod download
- name: test
@@ -28,6 +32,10 @@ steps:
JFA_GO_SNAPSHOT: y
JFA_GO_BUILT_BY:
from_secret: BUILT_BY
volumes:
- jfa-go-build-cache:/root/.cache/go-build
- jfa-go-mod-cache:/go/pkg/mod
- jfa-go-npm-cache:/npm
commands:
- make test
- name: build
@@ -36,10 +44,20 @@ steps:
JFA_GO_SNAPSHOT: y
JFA_GO_BUILT_BY:
from_secret: BUILT_BY
volumes:
- jfa-go-build-cache:/root/.cache/go-build
- jfa-go-mod-cache:/go/pkg/mod
- jfa-go-npm-cache:/npm
commands:
- curl -sfL https://goreleaser.com/static/run > goreleaser
- chmod +x goreleaser
- ./scripts/version.sh ./goreleaser --snapshot --skip=publish --clean
- ./scripts/version.sh goreleaser --snapshot --skip=publish --clean
- name: buildrone-binary
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
commands:
- wget https://builds.hrfee.pw/upload.py
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.zip ./dist/*.rpm ./dist/*.apk --tag internal-git=true'
- name: redoc
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
@@ -59,11 +77,43 @@ steps:
- bash -c 'ssh -i /tmp/id_repo root@apt.hrfee.dev -p 2022 "repo-process-deb trusty-unstable"'
- bash -c 'ssh -i /tmp/id_repo root@apt.hrfee.dev -p 2022 "repo-process-deb trusty"'
- bash -c 'ssh -i /tmp/id_repo root@apt.hrfee.dev -p 2022 "rm -f /repo/incoming/*.deb"'
- name: buildrone
- name: build-external
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
JFA_GO_SNAPSHOT: y
JFA_GO_BUILT_BY:
from_secret: BUILT_BY
volumes:
- jfa-go-build-cache:/root/.cache/go-build
- jfa-go-mod-cache:/go/pkg/mod
- jfa-go-npm-cache:/npm
commands:
- sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' ./build/data/html/setup.html
- env GOOS=linux INTERNAL=off ./scripts/version.sh goreleaser build --snapshot --id notray-e2ee --clean
- mv ./dist/notray-e2ee_linux_arm_6 ./dist/notray-e2ee_linux_arm
- name: container
image: docker.io/woodpeckerci/plugin-docker-buildx
settings:
dry_run: false
dockerfile: Dockerfile.ci
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_TOKEN
repo: docker.io/hrfee/jfa-go
tags: unstable
registry: docker.io
platforms: linux/amd64,linux/arm64,linux/arm/v7
build_args:
- BUILT_BY:
from_secret: BUILT_BY
- name: buildrone-container
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
commands:
- wget https://builds.hrfee.pw/upload.py
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.zip ./dist/*.rpm ./dist/*.apk --tag internal-git=true'
- python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-unstable=true

View File

@@ -1,26 +1,20 @@
# Use this instead if hrfee/jfa-go-build-docker doesn't support your architecture
# FROM --platform=$BUILDPLATFORM golang:latest AS support
FROM --platform=$BUILDPLATFORM docker.io/hrfee/jfa-go-build-docker:latest AS support
# FROM --platform=$BUILDPLATFORM jfa-go-bd AS support
ARG BUILT_BY
ENV JFA_GO_BUILT_BY=$BUILT_BY
COPY . /opt/build
# RUN curl -sfL https://goreleaser.com/static/run > /goreleaser && chmod +x /goreleaser
RUN cd /opt/build; INTERNAL=off UPDATER=docker ./scripts/version.sh /goreleaser build --snapshot --skip=validate --clean --id notray-e2ee
RUN cd /opt/build; INTERNAL=off UPDATER=docker ./scripts/version.sh goreleaser build --snapshot --skip=validate --clean --id notray-e2ee
RUN mv /opt/build/dist/*_linux_arm_6 /opt/build/dist/placeholder_linux_arm
RUN sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' /opt/build/build/data/html/setup.html
FROM golang:bookworm AS final
FROM gcr.io/distroless/base:latest AS final
ARG TARGETARCH
COPY --from=support /opt/build/dist/*_linux_${TARGETARCH}* /opt/jfa-go
COPY --from=support /opt/build/build/data /opt/jfa-go/data
RUN apt-get update -y && apt-get install libolm-dev -y
COPY --from=support /opt/build/dist/*_linux_${TARGETARCH}* /jfa-go
COPY --from=support /opt/build/build/data /jfa-go/data
EXPOSE 8056
EXPOSE 8057
CMD [ "/opt/jfa-go/jfa-go", "-data", "/data" ]
CMD [ "/jfa-go/jfa-go", "-data", "/data" ]

10
Dockerfile.ci Normal file
View File

@@ -0,0 +1,10 @@
FROM gcr.io/distroless/base:latest AS final
ARG TARGETARCH
COPY ./dist/notray-e2ee_linux_${TARGETARCH}* /jfa-go
COPY ./build/data /jfa-go/data
EXPOSE 8056
EXPOSE 8057
CMD [ "/jfa-go/jfa-go", "-data", "/data" ]

View File

@@ -1,6 +1,8 @@
.PHONY: configuration email typescript swagger copy compile compress inline-css variants-html install clean npm config-description config-default precompile test
.DEFAULT_GOAL := all
TS ?= npx tsgo
GOESBUILD ?= off
ifeq ($(GOESBUILD), on)
ESBUILD := esbuild
@@ -9,7 +11,7 @@ else
endif
GOBINARY ?= go
CSSVERSION ?= v0.6.0
CSSVERSION ?= $(shell git describe --tags --abbrev=0)
CSS_BUNDLE = $(DATA)/web/css/$(CSSVERSION)bundle.css
VERSION ?= $(shell git describe --exact-match HEAD 2> /dev/null || echo vgit)
@@ -33,7 +35,7 @@ E2EE ?= on
TAGS := -tags "
ifeq ($(INTERNAL), on)
DATA := data
DATA := build/data
COMPDEPS := $(BUILDDEPS)
else
DATA := build/data
@@ -46,7 +48,7 @@ ifeq ($(TRAY), on)
endif
ifeq ($(E2EE), on)
TAGS := $(TAGS) e2ee
TAGS := $(TAGS) e2ee goolm
endif
TAGS := $(TAGS)"
@@ -60,7 +62,7 @@ DEBUG ?= off
ifeq ($(DEBUG), on)
SOURCEMAP := --sourcemap
MINIFY :=
TYPECHECK := npx tsc -noEmit --project ts/tsconfig.json
TYPECHECK := $(TS) -noEmit --incremental --project ts/tsconfig.json
# jank
COPYTS := rm -r $(DATA)/web/js/ts; cp -r tempts $(DATA)/web/js/ts
UNCSS := cp $(CSS_BUNDLE) $(DATA)/bundle.css
@@ -102,6 +104,13 @@ else
SWAGINSTALL :=
endif
# FLAG HASHING: To rebuild on flag change.
# credit for idea to https://bnikolic.co.uk/blog/sh/make/unix/2021/07/08/makefile.html
rebuildFlags := GOESBUILD GOBINARY VERSION COMMIT UPDATER INTERNAL TRAY E2EE TAGS DEBUG RACE
rebuildVals := $(foreach v,$(rebuildFlags),$(v)=$($(v)))
rebuildHash := $(strip $(shell echo $(rebuildVals) | sha256sum | cut -d " " -f1))
rebuildHashFile := $(DATA)/buildhash-$(rebuildHash).txt
CONFIG_BASE = config/config-base.yaml
# CONFIG_DESCRIPTION = $(DATA)/config-base.json
@@ -116,7 +125,7 @@ $(DATA):
$(CONFIG_DEFAULT): $(CONFIG_BASE)
$(info Generating config-default.ini)
go run scripts/ini/main.go -in $(CONFIG_BASE) -out $(DATA)/config-default.ini
CGO_ENABLED=0 go run scripts/ini/main.go -in $(CONFIG_BASE) -out $(DATA)/config-default.ini
configuration: $(CONFIG_DEFAULT)
@@ -135,11 +144,14 @@ TYPESCRIPT_TEMPSRC = $(TYPESCRIPT_SRC:ts/%=tempts/%)
TYPESCRIPT_TARGET = $(DATA)/web/js/admin.js
$(TYPESCRIPT_TARGET): $(TYPESCRIPT_FULLSRC) ts/tsconfig.json
$(TYPECHECK)
# rm -rf tempts
# cp -r ts tempts
rm -rf tempts
cp -r ts tempts
mkdir -p tempts
$(adding dark variants to typescript)
scripts/dark-variant.sh tempts
scripts/dark-variant.sh tempts/modules
# scripts/dark-variant.sh tempts
# scripts/dark-variant.sh tempts/modules
CGO_ENABLED=0 go run scripts/variants/main.go -dir ts -out tempts
$(info compiling typescript)
$(foreach tempsrc,$(TYPESCRIPT_TEMPSRC),$(ESBUILD) --target=es6 --bundle $(tempsrc) $(SOURCEMAP) --outfile=$(patsubst %.ts,%.js,$(subst tempts/,./$(DATA)/web/js/,$(tempsrc))) $(MINIFY);)
$(COPYTS)
@@ -148,9 +160,9 @@ SWAGGER_SRC = $(wildcard api*.go) $(wildcard *auth.go) views.go
SWAGGER_TARGET = docs/docs.go
$(SWAGGER_TARGET): $(SWAGGER_SRC)
$(SWAGINSTALL)
swag init -g main.go
swag init --parseDependency --parseInternal -g main.go
VARIANTS_SRC = $(wildcard html/*.html)
VARIANTS_SRC = $(wildcard html/*.html) $(wildcard html/*.txt)
VARIANTS_TARGET = $(DATA)/html/admin.html
$(VARIANTS_TARGET): $(VARIANTS_SRC)
$(info copying html)
@@ -160,15 +172,24 @@ $(VARIANTS_TARGET): $(VARIANTS_SRC)
ICON_SRC = node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2
ICON_TARGET = $(ICON_SRC:node_modules/remixicon/fonts/%=$(DATA)/web/css/%)
SYNTAX_LIGHT_SRC = node_modules/highlight.js/styles/base16/atelier-sulphurpool-light.min.css
SYNTAX_LIGHT_TARGET = $(DATA)/web/css/$(CSSVERSION)highlightjs-light.css
SYNTAX_DARK_SRC = node_modules/highlight.js/styles/base16/circus.min.css
SYNTAX_DARK_TARGET = $(DATA)/web/css/$(CSSVERSION)highlightjs-dark.css
CODEINPUT_SRC = node_modules/@webcoder49/code-input/code-input.min.css
CODEINPUT_TARGET = $(DATA)/web/css/$(CSSVERSION)code-input.css
CSS_SRC = $(wildcard css/*.css)
CSS_TARGET = $(DATA)/web/css/part-bundle.css
CSS_FULLTARGET = $(CSS_BUNDLE)
ALL_CSS_SRC = $(ICON_SRC) $(CSS_SRC)
ALL_CSS_SRC = $(ICON_SRC) $(CSS_SRC) $(SYNTAX_LIGHT_SRC) $(SYNTAX_DARK_SRC)
ALL_CSS_TARGET = $(ICON_TARGET)
$(CSS_FULLTARGET): $(TYPESCRIPT_TARGET) $(VARIANTS_TARGET) $(ALL_CSS_SRC) $(wildcard html/*.html)
$(CSS_FULLTARGET): $(TYPESCRIPT_TARGET) $(VARIANTS_TARGET) $(ALL_CSS_SRC) $(wildcard html/*.html) $(wildcard html.*.txt)
$(info copying fonts)
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/
cp -r $(SYNTAX_LIGHT_SRC) $(SYNTAX_LIGHT_TARGET)
cp -r $(SYNTAX_DARK_SRC) $(SYNTAX_DARK_TARGET)
cp -r $(CODEINPUT_SRC) $(CODEINPUT_TARGET)
$(info bundling css)
rm -f $(CSS_TARGET) $(CSS_FULLTARGET)
$(ESBUILD) --bundle css/base.css --outfile=$(CSS_TARGET) --external:remixicon.css --external:../fonts/hanken* --minify
@@ -195,7 +216,7 @@ COPY_TARGET = $(DATA)/jfa-go.service
# $(DATA)/LICENSE $(LANG_TARGET) $(STATIC_TARGET) $(DATA)/web/css/$(CSSVERSION)bundle.css
$(COPY_TARGET): $(INLINE_TARGET) $(STATIC_SRC) $(LANG_SRC) $(CONFIG_BASE)
$(info copying $(CONFIG_BASE))
go run scripts/yaml/main.go -in $(CONFIG_BASE) -out $(DATA)/$(shell basename $(CONFIG_BASE))
CGO_ENABLED=0 go run scripts/yaml/main.go -in $(CONFIG_BASE) -out $(DATA)/$(shell basename $(CONFIG_BASE))
$(info copying crash page)
cp $(DATA)/crash.html $(DATA)/html/
$(info copying static data)
@@ -210,11 +231,16 @@ $(COPY_TARGET): $(INLINE_TARGET) $(STATIC_SRC) $(LANG_SRC) $(CONFIG_BASE)
BUILDDEPS := $(DATA) $(CONFIG_DEFAULT) $(EMAIL_TARGET) $(COPY_TARGET) $(SWAGGER_TARGET) $(INLINE_TARGET) $(CSS_FULLTARGET) $(TYPESCRIPT_TARGET)
precompile: $(BUILDDEPS)
COMPDEPS =
COMPDEPS = $(rebuildHashFile)
ifeq ($(INTERNAL), on)
COMPDEPS = $(BUILDDEPS)
COMPDEPS = $(BUILDDEPS) $(rebuildHashFile)
endif
$(rebuildHashFile):
$(info recording new flags $(rebuildVals))
rm -f $(DATA)/buildhash-*.txt
touch $(rebuildHashFile)
GO_SRC = $(shell find ./ -name "*.go")
GO_TARGET = build/jfa-go
$(GO_TARGET): $(COMPDEPS) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
@@ -222,12 +248,12 @@ $(GO_TARGET): $(COMPDEPS) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
$(GOBINARY) mod download
$(info Building)
mkdir -p build
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o $(GO_TARGET)
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o $(GO_TARGET) $(GOBUILDFLAGS)
test: $(BUILDDEPS) $(COMPDEPS) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
$(GOBINARY) test -ldflags="$(LDFLAGS)" $(TAGS) -p 1
all: $(BUILDDEPS) $(GO_TARGET)
all: $(BUILDDEPS) $(GO_TARGET) $(rebuildHashFile)
compress:
upx --lzma $(GO_TARGET)

View File

@@ -13,39 +13,28 @@
Studies mean I can't work on this project a lot outside of breaks, however I hope i'll be able to fit in general support and things like bug fixes into my time. New features and such will likely come in short bursts throughout the year (if they do at all).
#### Does/Will it still work?
jfa-go currently works on Jellyfin 10.11.0, the latest version as of 21/10/25. I should be able to maintain compatability in the future, unless any big changes occur.
jfa-go currently works on Jellyfin 10.11.0, the latest version as of 21/10/25. I should be able to maintain compatibility in the future, unless any big changes occur.
#### Alternatives
If you want a bit more of a guarantee of support, I've seen these projects mentioned although haven't tried them myself.
If you want a bit more guarantee of support [Wizarr](https://github.com/Wizarrrr/wizarr) is popular and seems very polished. It supports multiple media servers, lots of customization and invitation through Discord.
* [Wizarr](https://github.com/Wizarrrr/wizarr) focuses on invites, and also includes some Discord & Ombi integration.
* [Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is a fork of Overseerr which can manage users and mainly acts as an Ombi alternative.
* [jfa-go now integrates with Jellyseerr, much like Ombi, but better.](https://github.com/hrfee/jfa-go/pull/351)
* [Organizr](https://github.com/causefx/Organizr) doesn't focus on Jellyfin, but allows putting self-hosted services into "tabs" on a central page, and allows creating users, which lets one control who can access what.
---
jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) (and [Emby](https://emby.media/) as 2nd class) that provides invite-based account creation as well as other features that make one's instance much easier to manage.
jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) (and [Emby](https://emby.media/) as 2nd class) that provides invite-based account creation as well as other features that make ones instance much easier to manage.
#### Features
* 🧑 Invite based account creation: Send invites to your friends or family, and let them choose their own username and password without relying on you.
* Send invites via a link and/or email, discord, telegram or matrix
* Granular control over invites: Validity period as well as number of uses can be specified.
* Account profiles: Assign settings profiles to invites so new users have your predefined permissions, homescreen layout, etc. applied to their account on creation.
* Password validation: Ensure users choose a strong password.
* CAPTCHAs and contact method verificatoin can be enabled to avoid bots.
* ⌛ User expiry: Specify a validity period, and new users accounts will be disabled/deleted after it. The period can be manually extended too.
* 🔗 Ombi/Jellyseerr Integration: Automatically creates and synchronizes details for new accounts. Supports setting permissions with the Profiles feature. **Ombi integration use is risky, see [wiki](https://wiki.jfa-go.com/docs/ombi/)**.
* Account management: Bulk or individually; apply settings, delete, disable/enable, send messages and much more.
* 📣 Announcements: Bulk message your users with announcements about your server.
* Telegram/Discord/Matrix Integration: Verify users via a chat bot, and send Password Resets, Announcements, etc. through it.
* "My Account" Page: Allows users to reset their password, manage contact details, view their account expiry date, and send referrals. Can be customized with markdown.
* Referrals: Users can be given special invites to send to their friends and families, similar to some invite-only services like Bluesky.
* 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to them via email/telegram.
* Can also be done through the "My Account" page if enabled.
* Admin Notifications: Get notified when someone creates an account, or an invite expires.
* 🌓 Customizations
* Customize emails with variables and markdown
* Specify contact and help messages to appear in emails and pages
* Light and dark themes available
* **Invites**: Send invite links to new users so they can sign up without relying on you.
* Customize with profiles: Apply Jellyfin settings (library access, transcoding, etc.) on sign-up, with different profiles for each user type.
* Limit invites by time or number of uses, enforce strong passwords, require a CAPTCHA, and more
* **Password Resets**: Let your users do it themselves. Works with the Jellyfin "Forgot Password" feature, or through the "My Account" page. [See the wiki for your options](https://wiki.jfa-go.com/docs/pwr/).
* **Contact your users**: Collect email address, Discord/Telegram/Matrix info when the user signs up or add later, and jfa-go will contact them when needed (e.g. on/before account expiry, disabling/enabling, deletion) or when you wish with Markdown announcements.
* "Confirm email" optional, similar is required for Discord/Telegram/Matrix
* **"My Account"**: Lets your users change their password or email/contact info themselves and show them relevant info on a special page. Also,
* Referrals: Allow users a special, limited invite to give to their friends/family.
* **Advanced user management**: See all of your users at once and manage them in bulk (enable/disable/delete, send markdown announcements, apply profiles/settings, and more)
* User expiry: Set on an invite, and any new users will be valid for a fixed period (e.g. 30 days). After time passes, account is disabled, deleted, or disabled then deleted.
* **Ombi/Jellyseerr integration**: Sync username/passwords & contact details between your services.
* **Customizable**: Edit messages sent to users and shown on invites, "My Account" page and more with full Markdown support.
#### Interface
<p align="center">
@@ -57,7 +46,7 @@ jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jelly
#### Install
**Note**: `TrayIcon` builds include a tray icon to start/stop/restart, and an option to automatically start when you log-in to your computer. For Linux users, these builds depend on the `libappindicator3-1`/`libappindicator-gtk3`/`libappindicator` package for Debian/Ubuntu, Fedora, and Alpine respectively.
**Note**: `TrayIcon` builds include a tray icon to start/stop/restart, and an option to automatically start when you log-in to your computer.
`MatrixE2EE` builds (and Linux `TrayIcon` builds) include support for end-to-end encryption for the Matrix bot, but require the `libolm(-dev)` dependency. `.deb/.rpm/.apk` packages list this dependency, and docker images include it.

View File

@@ -99,7 +99,7 @@ func (app *appContext) generateActivitiesQuery(req ServerFilterReqDTO) *badgerho
for _, q := range req.Queries {
nq := q.AsDBQuery(query)
if nq == nil {
nq = ActivityDBQueryFromSpecialField(app.jf, query, q)
nq = ActivityDBQueryFromSpecialField(app.jf.MediaBrowser, query, q)
}
query = nq
}
@@ -156,8 +156,8 @@ func (app *appContext) GetActivities(gc *gin.Context) {
Value: act.Value,
Time: act.Time.Unix(),
IP: act.IP,
Username: act.MustGetUsername(app.jf),
SourceUsername: act.MustGetSourceUsername(app.jf),
Username: act.MustGetUsername(app.jf.MediaBrowser),
SourceUsername: act.MustGetSourceUsername(app.jf.MediaBrowser),
}
if act.Type == ActivityDeletion || act.Type == ActivityCreation {
// Username would've been in here, clear it to avoid confusion to the consumer

View File

@@ -1,6 +1,7 @@
package main
import (
"net/url"
"os"
"path/filepath"
"sort"
@@ -29,10 +30,15 @@ func (app *appContext) CreateBackup(gc *gin.Context) {
// @Security Bearer
// @tags Backups
func (app *appContext) GetBackup(gc *gin.Context) {
fname := gc.Param("fname")
escapedFName := gc.Param("fname")
fname, err := url.QueryUnescape(escapedFName)
if err != nil {
respondBool(400, false, gc)
return
}
// Hopefully this is enough to ensure the path isn't malicious. Hidden behind bearer auth anyway so shouldn't matter too much I guess.
b := Backup{}
err := b.FromString(fname)
err = b.FromString(fname)
if err != nil || b.Date.IsZero() {
app.debug.Printf(lm.IgnoreInvalidFilename, fname, err)
respondBool(400, false, gc)

View File

@@ -157,6 +157,184 @@ func (app *appContext) sendAdminExpiryNotification(data Invite) *sync.WaitGroup
return &wait
}
// @Summary Send an existing invite to an email address or discord user.
// @Produce json
// @Param SendInviteDTO body SendInviteDTO true "Email address or Discord username"
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /invites/send [post]
// @Security Bearer
// @tags Invites
func (app *appContext) SendInvite(gc *gin.Context) {
var req SendInviteDTO
gc.BindJSON(&req)
inv, ok := app.storage.GetInvitesKey(req.Invite)
if !ok {
app.err.Printf(lm.FailedGetInvite, req.Invite, lm.NotFound)
respond(500, "Invite not found", gc)
return
}
err := app.sendInvite(req.sendInviteDTO, &inv)
// Even if failed, some error info might have been stored in the invite.
app.storage.SetInvitesKey(req.Invite, inv)
if err != nil {
app.err.Printf(lm.FailedSendInviteMessage, req.Invite, req.SendTo, err)
respond(500, err.Error(), gc)
return
}
app.info.Printf(lm.SentInviteMessage, req.Invite, req.SendTo)
respondBool(200, true, gc)
}
// @Summary Edit an existing invite. Not all fields are modifiable.
// @Produce json
// @Param EditableInviteDTO body EditableInviteDTO true "Email address or Discord username"
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Failure 400 {object} stringResponse
// @Router /invites/edit [patch]
// @Security Bearer
// @tags Invites
func (app *appContext) EditInvite(gc *gin.Context) {
var req EditableInviteDTO
gc.BindJSON(&req)
inv, ok := app.storage.GetInvitesKey(req.Code)
if !ok {
msg := fmt.Sprintf(lm.InvalidInviteCode, req.Code)
app.err.Println(msg)
respond(400, msg, gc)
return
}
changed := false
if req.NotifyCreation != nil || req.NotifyExpiry != nil {
setNotify := map[string]bool{}
if req.NotifyExpiry != nil {
setNotify["notify-expiry"] = *req.NotifyExpiry
}
if req.NotifyCreation != nil {
setNotify["notify-creation"] = *req.NotifyCreation
}
ch, ok := app.SetNotify(&inv, setNotify, gc)
changed = changed || ch
if ch && !ok {
return
}
}
if req.Profile != nil {
ch, ok := app.SetProfile(&inv, *req.Profile, gc)
changed = changed || ch
if ch && !ok {
return
}
}
if req.Label != nil {
*req.Label = strings.TrimSpace(*req.Label)
changed = changed || (*req.Label != inv.Label)
inv.Label = *req.Label
}
if req.UserLabel != nil {
*req.UserLabel = strings.TrimSpace(*req.UserLabel)
changed = changed || (*req.UserLabel != inv.UserLabel)
inv.UserLabel = *req.UserLabel
}
if req.UserExpiry != nil {
changed = changed || (*req.UserExpiry != inv.UserExpiry)
inv.UserExpiry = *req.UserExpiry
if !inv.UserExpiry {
inv.UserMonths = 0
inv.UserDays = 0
inv.UserHours = 0
inv.UserMinutes = 0
}
}
if req.UserMonths != nil || req.UserDays != nil || req.UserHours != nil || req.UserMinutes != nil {
if inv.UserMonths == 0 &&
inv.UserDays == 0 &&
inv.UserHours == 0 &&
inv.UserMinutes == 0 {
changed = changed || (inv.UserExpiry != false)
inv.UserExpiry = false
}
if req.UserMonths != nil {
changed = changed || (*req.UserMonths != inv.UserMonths)
inv.UserMonths = *req.UserMonths
}
if req.UserDays != nil {
changed = changed || (*req.UserDays != inv.UserDays)
inv.UserDays = *req.UserDays
}
if req.UserHours != nil {
changed = changed || (*req.UserHours != inv.UserHours)
inv.UserHours = *req.UserHours
}
if req.UserMinutes != nil {
changed = changed || (*req.UserMinutes != inv.UserMinutes)
inv.UserMinutes = *req.UserMinutes
}
}
if changed {
app.storage.SetInvitesKey(inv.Code, inv)
}
respondBool(200, true, gc)
}
// sendInvite attempts to send an invite to the given email address or discord username.
func (app *appContext) sendInvite(req sendInviteDTO, invite *Invite) (err error) {
if !(app.config.Section("invite_emails").Key("enabled").MustBool(false)) {
// app.err.Printf(lm.FailedSendInviteMessage, invite.Code, req.SendTo, errors.New(lm.InviteMessagesDisabled))
err = errors.New(lm.InviteMessagesDisabled)
return err
}
discord := ""
if discordEnabled && (!strings.Contains(req.SendTo, "@") || strings.HasPrefix(req.SendTo, "@")) {
users := app.discord.GetUsers(req.SendTo)
if len(users) == 0 {
invite.SentTo.Failed = append(invite.SentTo.Failed, SendFailure{
Address: req.SendTo,
Reason: NoUser,
})
err = fmt.Errorf(lm.InvalidAddress, req.SendTo)
return err
} else if len(users) > 1 {
invite.SentTo.Failed = append(invite.SentTo.Failed, SendFailure{
Address: req.SendTo,
Reason: MultiUser,
})
err = fmt.Errorf(lm.InvalidAddress, req.SendTo)
return err
}
discord = users[0].User.ID
}
var msg *Message
msg, err = app.email.constructInvite(invite, false)
if err != nil {
// Slight misuse of the template
invite.SentTo.Failed = append(invite.SentTo.Failed, SendFailure{
Address: req.SendTo,
Reason: CheckLogs,
})
// app.err.Printf(lm.FailedConstructInviteMessage, req.SendTo, err)
return err
}
if discord != "" {
err = app.discord.SendDM(msg, discord)
} else {
err = app.email.send(msg, req.SendTo)
}
if err != nil {
invite.SentTo.Failed = append(invite.SentTo.Failed, SendFailure{
Address: req.SendTo,
Reason: CheckLogs,
})
return err
// app.err.Println(invite.SendTo)
}
invite.SentTo.Success = append(invite.SentTo.Success, req.SendTo)
return err
}
// @Summary Create a new invite.
// @Produce json
// @Param generateInviteDTO body generateInviteDTO true "New invite request object"
@@ -198,48 +376,11 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
}
invite.ValidTill = validTill
if req.SendTo != "" {
if !(app.config.Section("invite_emails").Key("enabled").MustBool(false)) {
app.err.Printf(lm.FailedSendInviteMessage, invite.Code, req.SendTo, errors.New(lm.InviteMessagesDisabled))
err := app.sendInvite(req.sendInviteDTO, &invite)
if err != nil {
app.err.Printf(lm.FailedSendInviteMessage, invite.Code, req.SendTo, err)
} else {
addressValid := false
discord := ""
if discordEnabled && (!strings.Contains(req.SendTo, "@") || strings.HasPrefix(req.SendTo, "@")) {
users := app.discord.GetUsers(req.SendTo)
if len(users) == 0 {
invite.SendTo = fmt.Sprintf(lm.FailedSendToTooltipNoUser, req.SendTo)
} else if len(users) > 1 {
invite.SendTo = fmt.Sprintf(lm.FailedSendToTooltipMultiUser, req.SendTo)
} else {
invite.SendTo = req.SendTo
addressValid = true
discord = users[0].User.ID
}
} else if emailEnabled {
addressValid = true
invite.SendTo = req.SendTo
}
if addressValid {
msg, err := app.email.constructInvite(invite, false)
if err != nil {
// Slight misuse of the template
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, req.SendTo, err)
app.err.Printf(lm.FailedConstructInviteMessage, invite.Code, err)
} else {
var err error
if discord != "" {
err = app.discord.SendDM(msg, discord)
} else {
err = app.email.send(msg, req.SendTo)
}
if err != nil {
invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, req.SendTo, err)
app.err.Println(invite.SendTo)
} else {
app.info.Printf(lm.SentInviteMessage, invite.Code, req.SendTo)
}
}
}
app.info.Printf(lm.SentInviteMessage, invite.Code, req.SendTo)
}
}
if req.Profile != "" {
@@ -312,31 +453,34 @@ func (app *appContext) GetInviteUsedCount(gc *gin.Context) {
// @Security Bearer
// @tags Invites,Statistics
func (app *appContext) GetInvites(gc *gin.Context) {
currentTime := time.Now()
// currentTime := time.Now()
app.checkInvites()
var invites []inviteDTO
for _, inv := range app.storage.GetInvites() {
if inv.IsReferral {
continue
}
years, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
months += years * 12
// years, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
// months += years * 12
invite := inviteDTO{
Code: inv.Code,
Months: months,
Days: days,
Hours: hours,
Minutes: minutes,
UserExpiry: inv.UserExpiry,
UserMonths: inv.UserMonths,
UserDays: inv.UserDays,
UserHours: inv.UserHours,
UserMinutes: inv.UserMinutes,
Created: inv.Created.Unix(),
Profile: inv.Profile,
NoLimit: inv.NoLimit,
Label: inv.Label,
UserLabel: inv.UserLabel,
EditableInviteDTO: EditableInviteDTO{
Code: inv.Code,
Label: &inv.Label,
UserLabel: &inv.UserLabel,
Profile: &inv.Profile,
UserExpiry: &inv.UserExpiry,
UserMonths: &inv.UserMonths,
UserDays: &inv.UserDays,
UserHours: &inv.UserHours,
UserMinutes: &inv.UserMinutes,
},
ValidTill: inv.ValidTill.Unix(),
// Months: months,
// Days: days,
// Hours: hours,
// Minutes: minutes,
Created: inv.Created.Unix(),
NoLimit: inv.NoLimit,
}
if len(inv.UsedBy) != 0 {
invite.UsedBy = map[string]int64{}
@@ -357,6 +501,9 @@ func (app *appContext) GetInvites(gc *gin.Context) {
if inv.RemainingUses != 0 {
invite.RemainingUses = inv.RemainingUses
}
if len(inv.SentTo.Success) != 0 || len(inv.SentTo.Failed) != 0 {
invite.SentTo = inv.SentTo
}
if inv.SendTo != "" {
invite.SendTo = inv.SendTo
}
@@ -369,10 +516,12 @@ func (app *appContext) GetInvites(gc *gin.Context) {
}
if _, ok := inv.Notify[addressOrID]; ok {
if _, ok = inv.Notify[addressOrID]["notify-expiry"]; ok {
invite.NotifyExpiry = inv.Notify[addressOrID]["notify-expiry"]
notifyExpiry := inv.Notify[addressOrID]["notify-expiry"]
invite.NotifyExpiry = &notifyExpiry
}
if _, ok = inv.Notify[addressOrID]["notify-creation"]; ok {
invite.NotifyCreation = inv.Notify[addressOrID]["notify-creation"]
notifyCreation := inv.Notify[addressOrID]["notify-creation"]
invite.NotifyCreation = &notifyCreation
}
}
}
@@ -384,82 +533,54 @@ func (app *appContext) GetInvites(gc *gin.Context) {
gc.JSON(200, resp)
}
// @Summary Set profile for an invite
// @Produce json
// @Param inviteProfileDTO body inviteProfileDTO true "Invite profile object"
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /invites/profile [post]
// @Security Bearer
// @tags Invites
func (app *appContext) SetProfile(gc *gin.Context) {
var req inviteProfileDTO
gc.BindJSON(&req)
func (app *appContext) SetProfile(inv *Invite, name string, gc *gin.Context) (changed, ok bool) {
changed = false
ok = false
// "" means "Don't apply profile"
if _, ok := app.storage.GetProfileKey(req.Profile); !ok && req.Profile != "" {
app.err.Printf(lm.FailedGetProfile, req.Profile)
if _, profileExists := app.storage.GetProfileKey(name); !profileExists && name != "" {
app.err.Printf(lm.FailedGetProfile, name)
respond(500, "Profile not found", gc)
return
}
inv, _ := app.storage.GetInvitesKey(req.Invite)
inv.Profile = req.Profile
app.storage.SetInvitesKey(req.Invite, inv)
respondBool(200, true, gc)
changed = name != inv.Profile
inv.Profile = name
ok = true
return
}
// @Summary Set notification preferences for an invite.
// @Produce json
// @Param setNotifyDTO body setNotifyDTO true "Map of invite codes to notification settings objects"
// @Success 200
// @Failure 400 {object} stringResponse
// @Failure 500 {object} stringResponse
// @Router /invites/notify [post]
// @Security Bearer
// @tags Other
func (app *appContext) SetNotify(gc *gin.Context) {
var req map[string]map[string]bool
gc.BindJSON(&req)
changed := false
for code, settings := range req {
invite, ok := app.storage.GetInvitesKey(code)
if !ok {
msg := fmt.Sprintf(lm.InvalidInviteCode, code)
app.err.Println(msg)
respond(400, msg, gc)
func (app *appContext) SetNotify(inv *Invite, settings map[string]bool, gc *gin.Context) (changed, ok bool) {
changed = false
ok = false
var address string
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(false)
if jellyfinLogin {
var addressAvailable bool = app.getAddressOrName(gc.GetString("jfId")) != ""
if !addressAvailable {
app.err.Printf(lm.FailedGetContactMethod, gc.GetString("jfId"))
respond(500, fmt.Sprintf(lm.FailedGetContactMethod, "admin"), gc)
return
}
var address string
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(false)
if jellyfinLogin {
var addressAvailable bool = app.getAddressOrName(gc.GetString("jfId")) != ""
if !addressAvailable {
app.err.Printf(lm.FailedGetContactMethod, gc.GetString("jfId"))
respond(500, fmt.Sprintf(lm.FailedGetContactMethod, "admin"), gc)
return
}
address = gc.GetString("jfId")
} else {
address = app.config.Section("ui").Key("email").String()
}
if invite.Notify == nil {
invite.Notify = map[string]map[string]bool{}
}
if _, ok := invite.Notify[address]; !ok {
invite.Notify[address] = map[string]bool{}
} /*else {
if _, ok := invite.Notify[address]["notify-expiry"]; !ok {
*/
for _, notifyType := range []string{"notify-expiry", "notify-creation"} {
if _, ok := settings[notifyType]; ok && invite.Notify[address][notifyType] != settings[notifyType] {
invite.Notify[address][notifyType] = settings[notifyType]
app.debug.Printf(lm.SetAdminNotify, notifyType, settings[notifyType], address)
changed = true
}
}
if changed {
app.storage.SetInvitesKey(code, invite)
address = gc.GetString("jfId")
} else {
address = app.config.Section("ui").Key("email").String()
}
if inv.Notify == nil {
inv.Notify = map[string]map[string]bool{}
}
if _, ok := inv.Notify[address]; !ok {
inv.Notify[address] = map[string]bool{}
} /*else {
if _, ok := invite.Notify[address]["notify-expiry"]; !ok {
*/
for _, notifyType := range []string{"notify-expiry", "notify-creation"} {
if _, ok := settings[notifyType]; ok && inv.Notify[address][notifyType] != settings[notifyType] {
inv.Notify[address][notifyType] = settings[notifyType]
app.debug.Printf(lm.SetAdminNotify, notifyType, settings[notifyType], address)
changed = true
}
}
ok = true
return
}
// @Summary Delete an invite.

View File

@@ -62,7 +62,7 @@ func (app *appContext) SetJellyseerrProfile(gc *gin.Context) {
}
u, err := app.js.UserByID(jellyseerrID)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyseerr, err)
app.err.Printf(lm.FailedGetUser, strconv.FormatInt(jellyseerrID, 10), lm.Jellyseerr, err)
respond(500, "Couldn't get user", gc)
return
}
@@ -112,6 +112,9 @@ func (js *JellyseerrWrapper) ImportUser(jellyfinID string, req newUserDTO, profi
return
}
ok = true
if !profile.Jellyseerr.Enabled {
return
}
err = js.ApplyTemplateToUser(jellyfinID, profile.Jellyseerr.User)
if err != nil {
err = fmt.Errorf(lm.FailedApplyTemplate, "user", lm.Jellyseerr, jellyfinID, err)

View File

@@ -1,6 +1,7 @@
package main
import (
"net/url"
"time"
"github.com/gin-gonic/gin"
@@ -158,7 +159,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
case "ExpiryReminder":
msg, err = app.email.constructExpiryReminder("", time.Now().AddDate(0, 0, 3), true)
case "InviteEmail":
msg, err = app.email.constructInvite(Invite{Code: ""}, true)
msg, err = app.email.constructInvite(&Invite{Code: ""}, true)
case "WelcomeEmail":
msg, err = app.email.constructWelcome("", time.Time{}, true)
case "EmailConfirmation":
@@ -169,6 +170,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
case "UserPage":
case "UserLogin":
case "PostSignupCard":
case "PreSignupCard":
// These don't have any example content
msg = nil
}
@@ -254,9 +256,11 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
return
}
tgUser := TelegramUser{
ChatID: tgToken.ChatID,
Username: tgToken.Username,
Contact: true,
TelegramVerifiedToken: TelegramVerifiedToken{
ChatID: tgToken.ChatID,
Username: tgToken.Username,
},
Contact: true,
}
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
tgUser.Lang = lang
@@ -581,8 +585,9 @@ func (app *appContext) MatrixConnect(gc *gin.Context) {
// @Security Bearer
// @tags Other
func (app *appContext) DiscordGetUsers(gc *gin.Context) {
name := gc.Param("username")
if name == "" {
escapedName := gc.Param("username")
name, err := url.QueryUnescape(escapedName)
if err != nil || name == "" {
respondBool(400, false, gc)
return
}

View File

@@ -12,17 +12,22 @@ import (
"github.com/hrfee/mediabrowser"
)
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, error) {
// getOmbiUser searches for an ombi user given a Jellyfin user ID. It looks for matching username or matching email address.
// If "email"=nil, an email address will be acquired from the DB instead. Passing it manually is useful when changing email address.
func (app *appContext) getOmbiUser(jfID string, email *string) (map[string]interface{}, error) {
jfUser, err := app.jf.UserByID(jfID, false)
if err != nil {
return nil, err
}
username := jfUser.Name
email := ""
if e, ok := app.storage.GetEmailsKey(jfID); ok {
email = e.Addr
if email == nil {
addr := ""
if e, ok := app.storage.GetEmailsKey(jfID); ok {
addr = e.Addr
}
email = &addr
}
user, err := app.ombi.getUser(username, email)
user, err := app.ombi.getUser(username, *email)
return user, err
}
@@ -147,7 +152,7 @@ func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
}
type OmbiWrapper struct {
OmbiUserByJfID func(jfID string) (map[string]interface{}, error)
OmbiUserByJfID func(jfID string, email *string) (map[string]interface{}, error)
*ombiLib.Ombi
}
@@ -191,7 +196,7 @@ func (ombi *OmbiWrapper) ImportUser(jellyfinID string, req newUserDTO, profile P
}
func (ombi *OmbiWrapper) SetContactMethods(jellyfinID string, email *string, discord *DiscordUser, telegram *TelegramUser, contactPrefs *common.ContactPreferences) (err error) {
ombiUser, err := ombi.OmbiUserByJfID(jellyfinID)
ombiUser, err := ombi.OmbiUserByJfID(jellyfinID, email)
if err != nil {
return
}

View File

@@ -2,6 +2,9 @@ package main
import (
"fmt"
"net/http"
"net/url"
"strconv"
"time"
"github.com/gin-gonic/gin"
@@ -69,6 +72,68 @@ func (app *appContext) GetProfiles(gc *gin.Context) {
gc.JSON(200, out)
}
// @Summary Get the raw values stored in a profile (Configuration, Policy, Jellyseerr/Ombi if applicable, etc.).
// @Produce json
// @Success 200 {object} ProfileDTO
// @Failure 400 {object} boolResponse
// @Param name path string true "name of profile (url encoded if necessary)"
// @Router /profiles/raw/{name} [get]
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) GetRawProfile(gc *gin.Context) {
escapedName := gc.Param("name")
name, err := url.QueryUnescape(escapedName)
if err != nil {
respondBool(400, false, gc)
return
}
if profile, ok := app.storage.GetProfileKey(name); ok {
gc.JSON(200, profile.ProfileDTO)
return
}
respondBool(400, false, gc)
}
// @Summary Update the raw data of a profile (Configuration, Policy, Jellyseerr/Ombi if applicable, etc.).
// @Produce json
// @Param ProfileDTO body ProfileDTO true "Raw profile data (all of it, do not omit anything)"
// @Success 204 {object} boolResponse
// @Success 201 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Router /profiles/raw/{name} [put]
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) ReplaceRawProfile(gc *gin.Context) {
escapedName := gc.Param("name")
name, err := url.QueryUnescape(escapedName)
if err != nil {
respondBool(400, false, gc)
return
}
existingProfile, ok := app.storage.GetProfileKey(name)
if !ok {
respondBool(400, false, gc)
return
}
var req ProfileDTO
gc.BindJSON(&req)
existingProfile.ProfileDTO = req
if req.Name == "" {
req.Name = name
}
status := http.StatusNoContent
app.storage.SetProfileKey(req.Name, existingProfile)
if req.Name != name {
// Name change
app.storage.DeleteProfileKey(name)
if discordEnabled {
app.discord.UpdateCommands()
}
status = http.StatusCreated
}
respondBool(status, true, gc)
}
// @Summary Set the default profile to use.
// @Produce json
// @Param profileChangeDTO body profileChangeDTO true "Default profile object"
@@ -119,7 +184,7 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
}
profile := Profile{
FromUser: user.Name,
Policy: user.Policy,
ProfileDTO: ProfileDTO{Policy: user.Policy},
Homescreen: req.Homescreen,
}
app.debug.Printf(lm.CreateProfileFromUser, user.Name)
@@ -132,6 +197,21 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
return
}
}
if req.Jellyseerr && app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
user, err := app.js.MustGetUser(req.ID)
if err != nil {
app.err.Printf(lm.FailedGetUser, user.Name(), lm.Jellyseerr, err)
} else {
profile.Jellyseerr.User = user.UserTemplate
n, err := app.js.GetNotificationPreferencesByID(user.ID)
if err != nil {
app.err.Printf(lm.FailedGetJellyseerrNotificationPrefs, strconv.FormatInt(user.ID, 10), err)
} else {
profile.Jellyseerr.Notifications = n.NotificationsTemplate
profile.Jellyseerr.Enabled = true
}
}
}
app.storage.SetProfileKey(req.Name, profile)
// Refresh discord bots, profile list
if discordEnabled {
@@ -167,7 +247,13 @@ func (app *appContext) DeleteProfile(gc *gin.Context) {
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) EnableReferralForProfile(gc *gin.Context) {
profileName := gc.Param("profile")
escapedProfileName := gc.Param("profile")
profileName, err := url.QueryUnescape(escapedProfileName)
if err != nil {
respond(400, "Invalid profile", gc)
app.err.Printf(lm.FailedGetProfile, profileName)
return
}
invCode := gc.Param("invite")
useExpiry := gc.Param("useExpiry") == "with-expiry"
inv, ok := app.storage.GetInvitesKey(invCode)
@@ -214,7 +300,13 @@ func (app *appContext) EnableReferralForProfile(gc *gin.Context) {
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) DisableReferralForProfile(gc *gin.Context) {
profileName := gc.Param("profile")
escapedProfileName := gc.Param("profile")
profileName, err := url.QueryUnescape(escapedProfileName)
if err != nil {
respond(400, "Invalid profile", gc)
app.err.Printf(lm.FailedGetProfile, profileName)
return
}
profile, ok := app.storage.GetProfileKey(profileName)
if !ok {
respondBool(200, true, gc)

View File

@@ -164,7 +164,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
var target ConfirmationTarget
var id string
fail := func() {
app.gcHTML(gc, 404, "404.html", OtherPage, gin.H{
app.gcHTML(gc, 404, "404.html", OtherPage, "en-us", gin.H{
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
@@ -199,22 +199,22 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
// Perform an Action
if target == NoOp {
gc.Redirect(http.StatusSeeOther, PAGES.MyAccount)
gc.Redirect(http.StatusSeeOther, MustGetNonEmptyURL(PAGES.MyAccount))
return
} else if target == UserEmailChange {
app.modifyEmail(id, claims["email"].(string))
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked,
UserID: gc.GetString("jfId"),
UserID: id,
SourceType: ActivityUser,
Source: gc.GetString("jfId"),
Source: id,
Value: "email",
Time: time.Now(),
}, gc, true)
app.info.Printf(lm.UserEmailAdjusted, gc.GetString("jfId"))
gc.Redirect(http.StatusSeeOther, PAGES.MyAccount)
app.info.Printf(lm.UserEmailAdjusted, id)
gc.Redirect(http.StatusSeeOther, MustGetNonEmptyURL(PAGES.MyAccount))
return
}
}
@@ -270,7 +270,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
} else if err := app.email.send(msg, req.Email); err != nil {
app.err.Printf(lm.FailedSendConfirmationEmail, id, req.Email, err)
} else {
app.err.Printf(lm.SentConfirmationEmail, id, req.Email)
app.info.Printf(lm.SentConfirmationEmail, id, req.Email)
}
return
}
@@ -394,9 +394,11 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
return
}
tgUser := TelegramUser{
ChatID: token.ChatID,
Username: token.Username,
Contact: true,
TelegramVerifiedToken: TelegramVerifiedToken{
ChatID: token.ChatID,
Username: token.Username,
},
Contact: true,
}
if lang, ok := app.telegram.languages[tgUser.ChatID]; ok {
tgUser.Lang = lang
@@ -714,7 +716,7 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
if app.config.Section("ombi").Key("enabled").MustBool(false) {
func() {
ombiUser, err := app.getOmbiUser(gc.GetString("jfId"))
ombiUser, err := app.getOmbiUser(gc.GetString("jfId"), nil)
if err != nil {
app.err.Printf(lm.FailedGetUser, user.Name, lm.Ombi, err)
return

View File

@@ -294,6 +294,7 @@ func (app *appContext) PostNewUserFromInvite(nu NewUserData, req ConfirmationKey
}
contactPrefs.Email = &(emailStore.Contact)
if profile != nil {
// FIXME: Why?
profile.ReferralTemplateKey = profile.ReferralTemplateKey
}
/// Ensures at least one contact method is enabled.
@@ -625,7 +626,12 @@ func (app *appContext) EnableReferralForUsers(gc *gin.Context) {
gc.BindJSON(&req)
mode := gc.Param("mode")
source := gc.Param("source")
escapedSource := gc.Param("source")
source, err := url.QueryUnescape(escapedSource)
if err != nil {
respondBool(400, false, gc)
return
}
useExpiry := gc.Param("useExpiry") == "with-expiry"
baseInv := Invite{}
if mode == "profile" {
@@ -813,13 +819,19 @@ func (app *appContext) GetAnnounceTemplate(gc *gin.Context) {
// @Summary Delete an announcement template.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param name path string true "name of template"
// @Param name path string true "name of template (url encoded if necessary)"
// @Router /users/announce/template/{name} [delete]
// @Security Bearer
// @tags Users
func (app *appContext) DeleteAnnounceTemplate(gc *gin.Context) {
name := gc.Param("name")
escapedName := gc.Param("name")
name, err := url.QueryUnescape(escapedName)
if err != nil {
respondBool(400, false, gc)
return
}
app.storage.DeleteAnnouncementsKey(name)
respondBool(200, false, gc)
}
@@ -891,60 +903,90 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) {
respondBool(204, true, gc)
}
// userSummary generates a respUser for to be displayed to the user, or sorted/filtered.
// also, consider it a source of which data fields/struct modifications need to trigger a cache invalidation.
func (app *appContext) userSummary(jfUser mediabrowser.User) respUser {
// userSummary functions the same as userSummary, but pulls from the given caches rather than the database.
func (app *appContext) userSummary(jfUser mediabrowser.User, email *EmailAddress, expiry *UserExpiry, discord *DiscordUser, telegram *TelegramUser, matrix *MatrixUser, referralActive bool) respUser {
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
allowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false)
user := respUser{
ID: jfUser.ID,
Name: jfUser.Name,
Admin: jfUser.Policy.IsAdministrator,
Disabled: jfUser.Policy.IsDisabled,
ReferralsEnabled: false,
ReferralsEnabled: referralActive || (email != nil && email.ReferralTemplateKey != ""),
}
if !jfUser.LastActivityDate.IsZero() {
user.LastActive = jfUser.LastActivityDate.Unix()
}
if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok {
if email != nil {
user.Email = email.Addr
user.NotifyThroughEmail = email.Contact
user.Label = email.Label
user.AccountsAdmin = (app.jellyfinLogin) && (email.Admin || (adminOnly && jfUser.Policy.IsAdministrator) || allowAll)
}
expiry, ok := app.storage.GetUserExpiryKey(jfUser.ID)
if ok {
if expiry != nil {
user.Expiry = expiry.Expiry.Unix()
}
if tgUser, ok := app.storage.GetTelegramKey(jfUser.ID); ok {
user.Telegram = tgUser.Username
user.NotifyThroughTelegram = tgUser.Contact
if telegram != nil {
user.Telegram = telegram.Username
user.NotifyThroughTelegram = telegram.Contact
}
if mxUser, ok := app.storage.GetMatrixKey(jfUser.ID); ok {
user.Matrix = mxUser.UserID
user.NotifyThroughMatrix = mxUser.Contact
if matrix != nil {
user.Matrix = matrix.UserID
user.NotifyThroughMatrix = matrix.Contact
}
if dcUser, ok := app.storage.GetDiscordKey(jfUser.ID); ok {
user.Discord = RenderDiscordUsername(dcUser)
// user.Discord = dcUser.Username + "#" + dcUser.Discriminator
user.DiscordID = dcUser.ID
user.NotifyThroughDiscord = dcUser.Contact
}
// FIXME: Send referral data
referrerInv := Invite{}
if referralsEnabled {
// 1. Directly attached invite.
err := app.storage.db.FindOne(&referrerInv, badgerhold.Where("ReferrerJellyfinID").Eq(jfUser.ID))
if err == nil {
user.ReferralsEnabled = true
// 2. Referrals via profile template. Shallow check, doesn't look for the thing in the database.
} else if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok && email.ReferralTemplateKey != "" {
user.ReferralsEnabled = true
}
if discord != nil {
user.Discord = RenderDiscordUsername(*discord)
// user.Discord = discord.Username + "#" + discord.Discriminator
user.DiscordID = discord.ID
user.NotifyThroughDiscord = discord.Contact
}
return user
}
// GetUserSummary generates a respUser for to be displayed to the user, or sorted/filtered.
// It fetches information from the db quite a lot. If calling lots, consider collecting data for all fields and calling app.userSummary().
// also, consider it a source of which data fields/struct modifications need to trigger a cache invalidation.
func (app *appContext) GetUserSummary(jfUser mediabrowser.User) respUser {
referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false)
var emailPtr *EmailAddress = nil
if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok {
emailPtr = &email
}
var expiryPtr *UserExpiry = nil
if expiry, ok := app.storage.GetUserExpiryKey(jfUser.ID); ok {
expiryPtr = &expiry
}
var discordPtr *DiscordUser = nil
if discordEnabled {
if discord, ok := app.storage.GetDiscordKey(jfUser.ID); ok {
discordPtr = &discord
}
}
var telegramPtr *TelegramUser = nil
if telegramEnabled {
if telegram, ok := app.storage.GetTelegramKey(jfUser.ID); ok {
telegramPtr = &telegram
}
}
var matrixPtr *MatrixUser = nil
if matrixEnabled {
if matrix, ok := app.storage.GetMatrixKey(jfUser.ID); ok {
matrixPtr = &matrix
}
}
referralsActive := false
// FIXME: Send referral data
referrerInv := Invite{}
// FIXME: This is veeery slow when running an arm64 binary through qemu
if referralsEnabled {
// 1. Directly attached invite.
if err := app.storage.db.FindOne(&referrerInv, badgerhold.Where("IsReferral").Eq(true).And("ReferrerJellyfinID").Eq(jfUser.ID)); err == nil {
referralsActive = true
}
// 2. performed by userSummaryFixme
}
return app.userSummary(jfUser, emailPtr, expiryPtr, discordPtr, telegramPtr, matrixPtr, referralsActive)
}
// @Summary Returns the total number of Jellyfin users.
@@ -952,7 +994,7 @@ func (app *appContext) userSummary(jfUser mediabrowser.User) respUser {
// @Success 200 {object} PageCountDTO
// @Router /users/count [get]
// @Security Bearer
// @tags Activity,Statistics
// @tags Users,Statistics
func (app *appContext) GetUserCount(gc *gin.Context) {
resp := PageCountDTO{}
users, err := app.jf.GetUsers(false)
@@ -965,6 +1007,52 @@ func (app *appContext) GetUserCount(gc *gin.Context) {
gc.JSON(200, resp)
}
// @Summary Returns the list of all labels on accounts.
// @Produce json
// @Success 200 {object} LabelsDTO
// @Router /users/labels [get]
// @Security Bearer
// @tags Users,Statistics
func (app *appContext) GetLabels(gc *gin.Context) {
if err := app.userCache.MaybeSync(app); err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Couldn't get users", gc)
return
}
gc.JSON(200, LabelsDTO{Labels: app.userCache.Labels})
}
// @Summary Get the details of a given user. Skips the user cache and gets fresh information.
// @Produce json
// @Success 200 {object} respUser
// @Failure 400 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Param id path string true "id of user to fetch details of"
// @Router /users/{id} [get]
// @Security Bearer
// @tags Users
func (app *appContext) GetUser(gc *gin.Context) {
userID := gc.Param("id")
if userID == "" {
respondBool(400, false, gc)
return
}
user, err := app.jf.UserByID(userID, false)
if err != nil {
switch err.(type) {
case mediabrowser.ErrUserNotFound:
respondBool(400, false, gc)
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
return
default:
respond(500, "Check logs", gc)
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
return
}
}
gc.JSON(200, app.GetUserSummary(user))
}
// @Summary Get a list of -all- Jellyfin users.
// @Produce json
// @Success 200 {object} getUsersDTO
@@ -1313,7 +1401,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
}
if ombi != nil {
errorString := ""
user, err := app.getOmbiUser(id)
user, err := app.getOmbiUser(id, nil)
if err != nil {
errorString += fmt.Sprintf("Ombi GetUser: %v ", err)
} else {
@@ -1362,3 +1450,100 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
app.InvalidateUserCaches()
gc.JSON(code, errors)
}
// @Summary Gets the number of Jellyfin/Emby activities stored by jfa-go related to the given user ID. As the total collected by jfa-go is limited, this may not include all those held by Jellyfin.
// @Produce json
// @Success 200 {object} PageCountDTO
// @Failure 400 {object} boolResponse
// @Param id path string true "id of user to fetch activities of."
// @Router /users/{id}/activities/jellyfin/count [get]
// @Security Bearer
// @tags Users
func (app *appContext) CountJFActivitesForUser(gc *gin.Context) {
userID := gc.Param("id")
if userID == "" {
respondBool(400, false, gc)
return
}
activities, err := app.jf.activity.ByUserID(userID)
if err != nil {
app.err.Printf(lm.FailedGetJFActivities, err)
respondBool(400, false, gc)
return
}
gc.JSON(200, PageCountDTO{Count: uint64(len(activities))})
}
// @Summary Get the latest Jellyfin/Emby activities related to the given user ID. Returns as many as the server has recorded. As the total collected by jfa-go is limited, this may not include all those held by Jellyfin.
// @Produce json
// @Success 200 {object} ActivityLogEntriesDTO
// @Failure 400 {object} boolResponse
// @Param id path string true "id of user to fetch activities of."
// @Router /users/{id}/activities/jellyfin [get]
// @Security Bearer
// @tags Users
func (app *appContext) GetJFActivitesForUser(gc *gin.Context) {
userID := gc.Param("id")
if userID == "" {
respondBool(400, false, gc)
return
}
activities, err := app.jf.activity.ByUserID(userID)
if err != nil {
app.err.Printf(lm.FailedGetJFActivities, err)
respondBool(400, false, gc)
return
}
out := ActivityLogEntriesDTO{
Entries: make([]ActivityLogEntryDTO, len(activities)),
}
for i := range activities {
out.Entries[i].ActivityLogEntry = activities[i]
out.Entries[i].Date = activities[i].Date.Unix()
}
app.debug.Printf(lm.GotNEntries, len(activities))
gc.JSON(200, out)
}
// @Summary Get the latest Jellyfin/Emby activities related to the given user ID, paginated. As the total collected by jfa-go is limited, this may not include all those held by Jellyfin.
// @Produce json
// @Param PaginatedReqDTO body PaginatedReqDTO true "pagination parameters"
// @Success 200 {object} PaginatedActivityLogEntriesDTO
// @Failure 400 {object} boolResponse
// @Param id path string true "id of user to fetch activities of."
// @Router /users/{id}/activities/jellyfin [post]
// @Security Bearer
// @tags Users
func (app *appContext) GetPaginatedJFActivitesForUser(gc *gin.Context) {
var req PaginatedReqDTO
gc.BindJSON(&req)
userID := gc.Param("id")
if userID == "" {
respondBool(400, false, gc)
return
}
activities, err := app.jf.activity.ByUserID(userID)
if err != nil {
app.err.Printf(lm.FailedGetJFActivities, err)
respondBool(400, false, gc)
return
}
out := PaginatedActivityLogEntriesDTO{}
startIndex := req.Page * req.Limit
if startIndex >= len(activities) {
out.LastPage = true
gc.JSON(200, out)
return
}
endIndex := min(startIndex+req.Limit, len(activities))
activities = activities[startIndex:endIndex]
out.Entries = make([]ActivityLogEntryDTO, len(activities))
out.LastPage = len(activities) != req.Limit
for i := range activities {
out.Entries[i].ActivityLogEntry = activities[i]
out.Entries[i].Date = activities[i].Date.Unix()
}
app.debug.Printf(lm.GotNEntries, len(activities))
gc.JSON(200, out)
}

2
api.go
View File

@@ -201,7 +201,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
respondBool(200, true, gc)
return
} */
ombiUser, err := app.getOmbiUser(user.ID)
ombiUser, err := app.getOmbiUser(user.ID, nil)
if err != nil {
app.err.Printf(lm.FailedGetUser, user.ID, lm.Ombi, err)
respondBool(200, true, gc)

View File

@@ -1,3 +1,4 @@
//go:build tray
// +build tray
package main
@@ -8,7 +9,7 @@ import (
"path/filepath"
"github.com/emersion/go-autostart"
"github.com/getlantern/systray"
"github.com/lutischan-ferenc/systray"
)
type Autostart struct {
@@ -48,8 +49,8 @@ func NewAutostart(name, displayname, trayName, trayTooltip string) *Autostart {
return a
}
func (a *Autostart) HandleCheck() {
for range a.menuitem.ClickedCh {
func (a *Autostart) Register() {
a.menuitem.Click(func() {
if !a.menuitem.Checked() {
if err := a.as.Enable(); err != nil {
log.Printf("Failed to enable autostart on login: %v", err)
@@ -65,5 +66,5 @@ func (a *Autostart) HandleCheck() {
log.Printf("Disabled autostart")
}
}
}
})
}

9
biome.json Normal file
View File

@@ -0,0 +1,9 @@
{
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 4,
"formatWithErrors": false,
"lineWidth": 120
}
}

View File

@@ -1,13 +1,14 @@
package common
type SectionMeta struct {
Name string `json:"name" yaml:"name" example:"My Section"` // friendly name of the section
Description string `json:"description" yaml:"description"`
Advanced bool `json:"advanced,omitempty" yaml:"advanced,omitempty"`
Disabled bool `json:"disabled,omitempty" yaml:"disabled,omitempty"`
DependsTrue string `json:"depends_true,omitempty" yaml:"depends_true,omitempty"`
DependsFalse string `json:"depends_false,omitempty" yaml:"depends_false,omitempty"`
WikiLink string `json:"wiki_link,omitempty" yaml:"wiki_link,omitempty"`
Name string `json:"name" yaml:"name" example:"My Section"` // friendly name of the section
Description string `json:"description" yaml:"description"`
Advanced bool `json:"advanced,omitempty" yaml:"advanced,omitempty"`
Disabled bool `json:"disabled,omitempty" yaml:"disabled,omitempty"`
DependsTrue string `json:"depends_true,omitempty" yaml:"depends_true,omitempty"`
DependsFalse string `json:"depends_false,omitempty" yaml:"depends_false,omitempty"`
WikiLink string `json:"wiki_link,omitempty" yaml:"wiki_link,omitempty"`
Aliases []string `json:"aliases,omitempty" yaml:"aliases,omitempty"`
}
type Option [2]string
@@ -40,6 +41,7 @@ type Setting struct {
Style string `json:"style,omitempty" yaml:"style,omitempty"`
Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
WikiLink string `json:"wiki_link,omitempty" yaml:"wiki_link,omitempty"`
Aliases []string `json:"aliases,omitempty" yaml:"aliases,omitempty"`
}
type Section struct {

View File

@@ -66,6 +66,13 @@ func FixFullURL(v string) string {
return v
}
func MustGetNonEmptyURL(path string) string {
if !strings.HasPrefix(path, "/") {
return "/" + path
}
return path
}
func FormatSubpath(path string, removeSingleSlash bool) string {
if path == "/" {
if removeSingleSlash {
@@ -202,8 +209,8 @@ func NewConfig(configPathOrContents any, dataPath string, logs LoggerSet) (*Conf
config.Section("email").Key("no_username").SetValue(strconv.FormatBool(config.Section("email").Key("no_username").MustBool(false)))
// FIXME: Remove all these, eventually
// config.MustSetValue("password_resets", "email_html", "jfa-go:"+"email.html")
// config.MustSetValue("password_resets", "email_text", "jfa-go:"+"email.txt")
// config.MustSetValue("password_resets", "email_html", "jfa-go:"+"password-reset.html")
// config.MustSetValue("password_resets", "email_text", "jfa-go:"+"password-reset.txt")
// config.MustSetValue("invite_emails", "email_html", "jfa-go:"+"invite-email.html")
// config.MustSetValue("invite_emails", "email_text", "jfa-go:"+"invite-email.txt")
@@ -291,6 +298,7 @@ func NewConfig(configPathOrContents any, dataPath string, logs LoggerSet) (*Conf
config.MustSetValue("jellyfin", "cache_timeout", "30")
config.MustSetValue("jellyfin", "web_cache_async_timeout", "1")
config.MustSetValue("jellyfin", "web_cache_sync_timeout", "10")
config.MustSetValue("jellyfin", "activity_cache_sync_timeout_seconds", "20")
LOGIP = config.Section("advanced").Key("log_ips").MustBool(false)
LOGIPU = config.Section("advanced").Key("log_ips_users").MustBool(false)
@@ -416,6 +424,7 @@ func (config *Config) ReloadDependents(app *appContext) {
}
app.email = NewEmailer(config, app.storage, app.LoggerSet)
}
func (app *appContext) ReloadConfig() {

View File

@@ -136,6 +136,13 @@ sections:
type: number
value: 10
description: "Synchronise after cache is this old, and wait for it: The accounts tab may take a little longer to load while it does."
- setting: activity_cache_sync_timeout
name: "Activity cache timeout (minutes)"
requires_restart: true
advanced: true
type: number
value: 0.1
description: "Synchronise Jellyfin's activity log after cache is this old. It can be pretty low as syncing only pulls new records and so is quick. Note this is unrelated to jfa-go's activity log."
- setting: type
name: Server type
requires_restart: true
@@ -739,19 +746,19 @@ sections:
etc.
- setting: use_24h
name: Use 24h time
depends_true: method
depends_true: enabled
type: bool
value: true
- setting: date_format
name: Date format
advanced: true
depends_true: method
advanced: false
depends_true: enabled
type: text
value: '%d/%m/%y'
description: Date format used in emails. Follows datetime.strftime format.
- setting: message
name: Help message
depends_true: method
depends_true: enabled
type: text
value: Need help? contact me.
description: Message displayed at bottom of emails.
@@ -1056,6 +1063,12 @@ sections:
value: en-us
description: Default telegram message language. Visit weblate if you'd like to
translate.
- setting: ignore_client_language
name: Always use default language
depends_true: enabled
type: bool
value: false
description: When disabled, jfa-go will check the telegram user's language and use it if possible. Enable to ignore it and use your configured default language always.
- section: matrix
meta:
name: Matrix
@@ -1368,6 +1381,10 @@ sections:
and telegram). A template must be added to a User Profile for accounts to be
created.
wiki_link: https://wiki.jfa-go.com/docs/external-services/jellyseerr/
aliases:
- Jellyseerr
- Overseerr
- Seerr
settings:
- setting: enabled
name: Enabled
@@ -1403,6 +1420,7 @@ sections:
depends_true: enabled
description: Existing users (and those created outside jfa-go) will have their
contact info imported to Jellyseerr.
deprecated: true
- setting: constraints_note
name: 'Unique Emails:'
type: note

View File

@@ -66,9 +66,6 @@ html:not(.dark) .card.\@low:not(.\~neutral):not(.\~positive):not(.\~urge):not(.\
}
@media screen and (max-width: 1024px) {
:root {
font-size: 0.9rem;
}
.table-responsive table {
min-width: 800px;
}
@@ -207,9 +204,9 @@ sup.\~critical, .text-critical {
font-size: 1rem;
padding-top: 0.1rem;
padding-bottom: 0.1rem;
margin-left: 0.5rem;
margin-right: 1rem;
max-width: 75%;
margin-inline-start: 0.5rem;
margin-inline-end: 1rem;
width: 5rem;;
}
.stealth-input-hidden {
@@ -235,7 +232,7 @@ sup.\~critical, .text-critical {
margin-bottom: 0.25rem;
}
.textarea {
.textarea:not(code-input *) {
resize: vertical;
}
@@ -247,7 +244,7 @@ sup.\~critical, .text-critical {
overflow-y: visible;
}
select, textarea {
select, textarea:not(code-input *) {
color: inherit;
border: 0 solid var(--color-neutral-300);
appearance: none;
@@ -255,7 +252,7 @@ select, textarea {
-moz-appearance: none;
}
html.dark textarea {
html.dark textarea:not(code-input *) {
background-color: #202020
}
@@ -279,13 +276,12 @@ table.table-p-0 th, table.table-p-0 td {
padding-bottom: 0 !important;
}
p.top {
margin-top: 0px;
td:dir(rtl), th:dir(rtl) {
text-align: right;
}
.table-responsive {
overflow-x: auto;
font-size: 0.9rem;
p.top {
margin-top: 0px;
}
#notification-box {
@@ -313,7 +309,7 @@ p.top {
bottom: 115%;
}
pre {
pre:not(code-input *) {
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
@@ -459,13 +455,13 @@ section.section:not(.\~neutral) {
@layer components {
.switch {
@apply flex flex-row gap-1 items-center;
@apply flex flex-row gap-2 items-center;
}
}
:root {
/* seems to be the sweet spot */
--inside-input-base: -2.6rem;
--inside-input-base: -2.1rem;
/* thought --spacing would do the trick but apparently not */
--tailwind-spacing: 0.25rem;
@@ -473,9 +469,26 @@ section.section:not(.\~neutral) {
/* places buttons inside a sibling input element (hopefully), based on the flex gap of the parent. */
.gap-1 > .button.inside-input {
margin-left: calc(var(--inside-input-base) - 1.0*var(--tailwind-spacing));
margin-inline-start: calc(var(--inside-input-base) - 1.0*var(--tailwind-spacing));
}
.gap-2 > .button.inside-input {
margin-left: calc(var(--inside-input-base) - 2.0*var(--tailwind-spacing));
margin-inline-start: calc(var(--inside-input-base) - 2.0*var(--tailwind-spacing));
}
.force-ltr {
direction: ltr !important;
}
.content ul, .content ol {
margin-left: unset;
margin-inline-start: 2rem;
}
.content li {
text-align: start;
}
.input {
font-size: 16px !important;
}

View File

@@ -29,7 +29,7 @@ html:not(.dark) .wall {
}
.modal-close {
float: right;
float: inline-end;
color: #aaa;
font-weight: normal;
}

View File

@@ -44,10 +44,20 @@
left: 120%;
}
.tooltip.right:dir(rtl):not(.force-ltr) .content {
right: 120%;
left: unset;
}
.tooltip.left .content {
right: 120%;
}
.tooltip.left:dir(rtl):not(.force-ltr) .content {
left: 120%;
right: unset;
}
.tooltip .content.sm {
font-size: 0.8rem;
}

View File

@@ -352,9 +352,33 @@ var customContent = map[string]CustomContentInfo{
"myAccountURL",
),
Placeholders: defaultVals(map[string]any{
"myAccountURL": "https://sub2.test.url/my/account",
"myAccountURL": "https://example.url/my/account",
}),
},
"PreSignupCard": {
Name: "PreSignupCard",
ContentType: CustomCard,
DisplayName: func(dict *Lang, lang string) string {
if _, ok := dict.Admin[lang]; !ok {
lang = dict.chosenAdminLang
}
return dict.Admin[lang].Strings["preSignupCard"]
},
Description: func(dict *Lang, lang string) string {
if _, ok := dict.Admin[lang]; !ok {
lang = dict.chosenAdminLang
}
return dict.Admin[lang].Strings["preSignupCardDescription"]
},
Variables: []string{
"myAccountURL",
"profile",
},
Placeholders: map[string]any{
"myAccountURL": "https://example.url/my/account",
"profile": "Default User Profile",
},
},
}
var EmptyCustomContent = CustomContentInfo{

View File

@@ -738,7 +738,6 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
var invname *dg.Member = nil
invname, err = d.bot.GuildMember(d.guildID, recipient.ID)
invite.SendTo = invname.User.Username
if err == nil && !(d.app.config.Section("invite_emails").Key("enabled").MustBool(false)) {
err = errors.New(lm.InviteMessagesDisabled)
@@ -746,11 +745,14 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
var msg *Message
if err == nil {
msg, err = d.app.email.constructInvite(invite, false)
msg, err = d.app.email.constructInvite(&invite, false)
if err != nil {
// Print extra message, ideally we'd just print this, or get rid of it though.
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, invite.Code, err)
d.app.err.Println(invite.SendTo)
invite.SentTo.Failed = append(invite.SentTo.Failed, SendFailure{
Address: invname.User.Username,
Reason: CheckLogs,
})
d.app.err.Printf(lm.FailedConstructInviteMessage, invite.Code, err)
}
}
@@ -760,12 +762,12 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
if err == nil {
d.app.info.Printf(lm.SentInviteMessage, invite.Code, RenderDiscordUsername(recipient))
invite.SentTo.Success = append(invite.SentTo.Success, invname.User.Username)
sendResponse("sentInvite")
}
if err != nil {
invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, RenderDiscordUsername(recipient), err)
d.app.err.Println(invite.SendTo)
sendResponse("sentInviteFailure")
}
}

View File

@@ -1,9 +1,7 @@
module github.com/hrfee/jfa-go/easyproxy
go 1.23.0
go 1.24.0
toolchain go1.24.5
require golang.org/x/net v0.42.0
require golang.org/x/net v0.47.0
require github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b

View File

@@ -1,6 +1,4 @@
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b h1:xZ59n7Frzh8CwyfAapUZLSg+gXH5m63YEaFCMpDHhpI=
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b/go.mod h1:uDd4sYVYsqcxAB8j+Q7uhL6IJCs/r1kxib1HV4bgOMg=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=

View File

@@ -19,6 +19,8 @@ import (
textTemplate "text/template"
"time"
sTemplate "github.com/hrfee/simple-template"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/hrfee/jfa-go/easyproxy"
@@ -251,13 +253,13 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten
Subject: contentInfo.Subject(emailer.config, &emailer.lang),
}
// Template the subject for bonus points
if subject, err := templateEmail(msg.Subject, contentInfo.Variables, contentInfo.Conditionals, data); err == nil {
if subject, err := sTemplate.Template(msg.Subject, data); err == nil {
msg.Subject = subject
}
if cc.Enabled {
// Use template email, rather than the built-in's email file.
contentInfo.SourceFile = customContent["TemplateEmail"].SourceFile
content, err := templateEmail(cc.Content, contentInfo.Variables, contentInfo.Conditionals, data)
content, err := sTemplate.Template(cc.Content, data)
if err != nil {
emailer.err.Printf(lm.FailedConstructCustomContent, msg.Subject, err)
return msg, err
@@ -377,7 +379,7 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, placeh
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructInvite(invite Invite, placeholders bool) (*Message, error) {
func (emailer *Emailer) constructInvite(invite *Invite, placeholders bool) (*Message, error) {
expiry := invite.ValidTill
d, t, expiresIn := emailer.formatExpiry(expiry, false)
inviteLink := fmt.Sprintf("%s%s/%s", ExternalURI(nil), PAGES.Form, invite.Code)

View File

@@ -180,7 +180,7 @@ func TestInvite(t *testing.T) {
Created: time.Now(),
ValidTill: time.Now().Add(30 * time.Minute),
}
msg, err := e.constructInvite(inv, false)
msg, err := e.constructInvite(&inv, false)
if err != nil {
t.Fatalf("failed construct: %+v", err)
}

34
go.mod
View File

@@ -20,16 +20,18 @@ replace github.com/hrfee/jfa-go/easyproxy => ./easyproxy
replace github.com/hrfee/jfa-go/jellyseerr => ./jellyseerr
// replace github.com/hrfee/mediabrowser => ../mediabrowser
require (
github.com/bwmarrin/discordgo v0.29.0
github.com/dgraph-io/badger/v4 v4.8.0
github.com/emersion/go-autostart v0.0.0-20250403115856-34830d6457d2
github.com/fatih/color v1.18.0
github.com/fsnotify/fsnotify v1.9.0
github.com/getlantern/systray v1.2.2
github.com/gin-contrib/pprof v1.5.3
github.com/gin-gonic/gin v1.11.0
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/goccy/go-yaml v1.18.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a
github.com/hrfee/jfa-go/common v0.0.0-20251123165523-7c9f91711460
@@ -40,9 +42,11 @@ require (
github.com/hrfee/jfa-go/logger v0.0.0-20251123165523-7c9f91711460
github.com/hrfee/jfa-go/logmessages v0.0.0-20251123165523-7c9f91711460
github.com/hrfee/jfa-go/ombi v0.0.0-20251123165523-7c9f91711460
github.com/hrfee/mediabrowser v0.3.30
github.com/hrfee/mediabrowser v0.3.36
github.com/hrfee/simple-template v1.1.0
github.com/itchyny/timefmt-go v0.1.7
github.com/lithammer/shortuuid/v3 v3.0.7
github.com/lutischan-ferenc/systray v1.2.1
github.com/mailgun/mailgun-go/v4 v4.23.0
github.com/mattn/go-sqlite3 v1.14.32
github.com/robert-nix/ansihtml v1.0.1
@@ -53,7 +57,6 @@ require (
github.com/writeas/go-strip-markdown v2.0.1+incompatible
github.com/xhit/go-simple-mail/v2 v2.16.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.26.0
)
@@ -65,17 +68,9 @@ require (
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgraph-io/ristretto v1.0.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect
github.com/getlantern/errors v1.0.4 // indirect
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 // indirect
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -83,7 +78,6 @@ require (
github.com/go-openapi/jsonpointer v0.22.3 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/spec v0.22.1 // indirect
github.com/go-openapi/swag v0.25.3 // indirect
github.com/go-openapi/swag/conv v0.25.3 // indirect
github.com/go-openapi/swag/jsonname v0.25.3 // indirect
github.com/go-openapi/swag/jsonutils v0.25.3 // indirect
@@ -94,39 +88,32 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/go-test/deep v1.1.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/godbus/dbus/v5 v5.2.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/flatbuffers v25.9.23+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b // indirect
github.com/mailgun/errors v0.4.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.0 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/swaggo/swag v1.16.6 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
@@ -135,14 +122,11 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mau.fi/util v0.9.3 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
@@ -151,7 +135,7 @@ require (
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect

301
go.sum
View File

@@ -11,24 +11,12 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k=
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -38,14 +26,8 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
@@ -57,25 +39,17 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v4 v4.1.0/go.mod h1:P50u28d39ibBRmIJuQC/NSdBOg46HnHw7al2SW5QRHg=
github.com/dgraph-io/badger/v4 v4.3.1 h1:7r5wKqmoRpGgSxqa0S/nGdpOpvvzuREGPLSua73C8tw=
github.com/dgraph-io/badger/v4 v4.3.1/go.mod h1:oObz97DImXpd6O/Dt8BqdKLLTDmEmarAimo72VV5whQ=
github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs=
github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84=
github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:M88ob4TyDnEqNuL3PgsE/p3bDujfspnulR+0dQWNYZs=
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:buzQsO8HHkZX2Q45fdfGH1xejPjuDQaXH8btcYMFzPM=
github.com/emersion/go-autostart v0.0.0-20250403115856-34830d6457d2 h1:CgF8+TNFvlnxEbplSgS70ZI4IUFEzVkY+ICNqTVE/AM=
github.com/emersion/go-autostart v0.0.0-20250403115856-34830d6457d2/go.mod h1:buzQsO8HHkZX2Q45fdfGH1xejPjuDQaXH8btcYMFzPM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -86,70 +60,28 @@ github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA=
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/errors v1.0.4 h1:i2iR1M9GKj4WuingpNqJ+XQEw6i6dnAgKAmLj6ZB3X0=
github.com/getlantern/errors v1.0.4/go.mod h1:/Foq8jtSDGP8GOXzAjeslsC4Ar/3kB+UiQH+WyV4pzY=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 h1:NlQedYmPI3pRAXJb+hLVVDGqfvvXGRPV8vp7XOjKAZ0=
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65/go.mod h1:+ZU1h+iOVqWReBpky6d5Y2WL0sF2Llxu+QcxJFs2+OU=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc h1:sue+aeVx7JF5v36H1HfvcGFImLpSD5goj8d+MitovDU=
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc/go.mod h1:D9RWpXy/EFPYxiKUURo2TB8UBosbqkiLhttRrZYtvqM=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 h1:cSrD9ryDfTV2yaur9Qk3rHYD414j3Q1rl7+L0AylxrE=
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770/go.mod h1:GOQsoDnEHl6ZmNIL+5uVo+JWRFWozMEp18Izcb++H+A=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 h1:3BwvWj0JZzFEvNNiMhCu4bf60nqcIuQpTYb00Ezm1ag=
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534/go.mod h1:ZsLfOY6gKQOTyEcPYNA9ws5/XHZQFroxqCOhHjGcs9Y=
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/pprof v1.5.0 h1:E/Oy7g+kNw94KfdCy3bZxQFtyDnAX2V7axRS7sNYVrU=
github.com/gin-contrib/pprof v1.5.0/go.mod h1:GqFL6LerKoCQ/RSWnkYczkTJ+tOAUVN/8sbnEtaqOKs=
github.com/gin-contrib/pprof v1.5.3 h1:Bj5SxJ3kQDVez/s/+f9+meedJIqLS+xlkIVDe/lcvgM=
github.com/gin-contrib/pprof v1.5.3/go.mod h1:0+LQSZ4SLO0B6+2n6JBzaEygpTBxe/nI+YEYpfQQ6xY=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -157,41 +89,30 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag v0.25.3 h1:FAa5wJXyDtI7yUztKDfZxDrSx+8WTg31MfCQ9s3PV+s=
github.com/go-openapi/swag v0.25.3/go.mod h1:tX9vI8Mj8Ny+uCEk39I1QADvIPI7lkndX4qCsEqhkS8=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.3 h1:PcB18wwfba7MN5BVlBIV+VxvUUeC2kEuCEyJ2/t2X7E=
github.com/go-openapi/swag/conv v0.25.3/go.mod h1:n4Ibfwhn8NJnPXNRhBO5Cqb9ez7alBR40JS4rbASUPU=
github.com/go-openapi/swag/jsonname v0.25.3 h1:U20VKDS74HiPaLV7UZkztpyVOw3JNVsit+w+gTXRj0A=
github.com/go-openapi/swag/jsonname v0.25.3/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.3 h1:kV7wer79KXUM4Ea4tBdAVTU842Rg6tWstX3QbM4fGdw=
github.com/go-openapi/swag/jsonutils v0.25.3/go.mod h1:ILcKqe4HC1VEZmJx51cVuZQ6MF8QvdfXsQfiaCs0z9o=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.3 h1:/i3E9hBujtXfHy91rjtwJ7Fgv5TuDHgnSrYjhFxwxOw=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.3/go.mod h1:8kYfCR2rHyOj25HVvxL5Nm8wkfzggddgjZm6RgjT8Ao=
github.com/go-openapi/swag/loading v0.25.3 h1:Nn65Zlzf4854MY6Ft0JdNrtnHh2bdcS/tXckpSnOb2Y=
github.com/go-openapi/swag/loading v0.25.3/go.mod h1:xajJ5P4Ang+cwM5gKFrHBgkEDWfLcsAKepIuzTmOb/c=
github.com/go-openapi/swag/stringutils v0.25.3 h1:nAmWq1fUTWl/XiaEPwALjp/8BPZJun70iDHRNq/sH6w=
@@ -200,33 +121,29 @@ github.com/go-openapi/swag/typeutils v0.25.3 h1:2w4mEEo7DQt3V4veWMZw0yTPQibiL3ri
github.com/go-openapi/swag/typeutils v0.25.3/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.3 h1:LKTJjCn/W1ZfMec0XDL4Vxh8kyAnv1orH5F2OREDUrg=
github.com/go-openapi/swag/yamlutils v0.25.3/go.mod h1:Y7QN6Wc5DOBXK14/xeo1cQlq0EA0wvLoSv13gDQoCao=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
@@ -236,10 +153,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
github.com/golang/glog v1.1.1/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -253,22 +167,12 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 h1:5lyLWsV+qCkoYqsKUDuycESh9DEIPVKN6iCFeL7ag50=
github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v23.5.9+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v25.9.23+incompatible h1:rGZKv+wOb6QPzIdkM2KxhBZCDrA0DeN6DNmRDrqIsQU=
github.com/google/flatbuffers v25.9.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -279,9 +183,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -291,19 +194,19 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hrfee/mediabrowser v0.3.28 h1:KkSgODXxUnZLrkmjSWpma8mXwEVxlOtI51uS2QP/e+c=
github.com/hrfee/mediabrowser v0.3.28/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.29 h1:xTqGS9u8HuolZAhouYHxutnE0fF/8aVCInbByKZEzIo=
github.com/hrfee/mediabrowser v0.3.29/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.30 h1:llJo4hxWchbwROnkfhlYsrvtZ6/8WDTp3QxAvbgjUfI=
github.com/hrfee/mediabrowser v0.3.30/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.33 h1:kjUFZc46hNhbOEU4xZNyhGVNjfZ5lENmX95Md1thxiA=
github.com/hrfee/mediabrowser v0.3.33/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.34 h1:AKnd1V9wt+KWZmHDjj1GMkCgcgcpBKxPw5iUcYgD6Tg=
github.com/hrfee/mediabrowser v0.3.34/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.35 h1:xEq4cL96Di0G+S3ONBH1HHeQJU6IfUMZiaeGeuJSFS8=
github.com/hrfee/mediabrowser v0.3.35/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.36 h1:erYWzmaz4b6wfxEfeEWQHLMI9fSpGDy1m7NxkQmIVsw=
github.com/hrfee/mediabrowser v0.3.36/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/simple-template v1.1.0 h1:PNQDTgc2H0s19/pWuhRh4bncuNJjPrW0fIX77YtY78M=
github.com/hrfee/simple-template v1.1.0/go.mod h1:s9a5QgfqbmT7j9WCC3GD5JuEqvihBEohyr+oYZmr4bA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
github.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA=
github.com/itchyny/timefmt-go v0.1.7/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -312,18 +215,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -336,28 +231,19 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/lutischan-ferenc/systray v1.2.1 h1:gPNrEpmg4hMwXyKNSlrkuuXqvxgqCYPjF5H/pG9I1+c=
github.com/lutischan-ferenc/systray v1.2.1/go.mod h1:YYaJ28AVuhMrlI5JfqrMsYMIl3Aa4Q02bpXXCl9caqo=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b h1:xZ59n7Frzh8CwyfAapUZLSg+gXH5m63YEaFCMpDHhpI=
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b/go.mod h1:uDd4sYVYsqcxAB8j+Q7uhL6IJCs/r1kxib1HV4bgOMg=
github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8=
github.com/mailgun/errors v0.4.0/go.mod h1:xGBaaKdEdQT0/FhwvoXv4oBaqqmVZz9P1XEnvD/onc0=
github.com/mailgun/mailgun-go/v4 v4.18.1 h1:ShNH/wzj7albTF/6le011FF+DGMd3azcSKL4iO9AgeI=
github.com/mailgun/mailgun-go/v4 v4.18.1/go.mod h1:+d4FCswFAukgYc1XtKK2IxOYaVxjVm8AN2z/5TBiT8M=
github.com/mailgun/mailgun-go/v4 v4.23.0 h1:jPEMJzzin2s7lvehcfv/0UkyBu18GvcURPr2+xtZRbk=
github.com/mailgun/mailgun-go/v4 v4.23.0/go.mod h1:imTtizoFtpfZqPqGP8vltVBB6q9yWcv6llBhfFeElZU=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -369,10 +255,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -383,21 +265,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/petermattis/goid v0.0.0-20241025130422-66cb2e6d7274 h1:qli3BGQK0tYDkSEvZ/FzZTi9ZrOX86Q6CIhKLGc489A=
github.com/petermattis/goid v0.0.0-20241025130422-66cb2e6d7274/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb h1:3PrKuO92dUTMrQ9dx0YNejC6U/Si6jqKmyQ9vWjwqR4=
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a h1:VweslR2akb/ARhXfqSfRbj1vpWwYXf3eeAUyw/ndms0=
github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -408,12 +281,9 @@ github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3Adq
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc=
github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
@@ -422,7 +292,6 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
@@ -445,29 +314,26 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI=
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=
github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c h1:coVla7zpsycc+kA9NXpcvv2E4I7+ii6L5hZO2S6C3kw=
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
@@ -479,8 +345,6 @@ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6
github.com/timshannon/badgerhold/v4 v4.0.3 h1:W6pd2qckoXw2cl8eH0ZCV/9CXNaXvaM26tzFi5Tj+v8=
github.com/timshannon/badgerhold/v4 v4.0.3/go.mod h1:IkZIr0kcZLMdD7YJfW/G6epb6ZXHD/h0XR2BTk/VZg8=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 h1:flbMkdl6HxQkLs6DDhH1UkcnFpNBOu70391STjMS0O4=
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5JNfgjzpzvYLHjH0QOgPZPYnRWGA=
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
@@ -490,10 +354,6 @@ github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/o
github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
@@ -505,61 +365,23 @@ github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4te
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mau.fi/util v0.8.1 h1:Ga43cz6esQBYqcjZ/onRoVnYWoUwjWbsxVeJg2jOTSo=
go.mau.fi/util v0.8.1/go.mod h1:T1u/rD2rzidVrBLyaUdPpZiJdP/rsyi+aTzn0D+Q6wc=
go.mau.fi/util v0.8.8 h1:OnuEEc/sIJFhnq4kFggiImUpcmnmL/xpvQMRu5Fiy5c=
go.mau.fi/util v0.8.8/go.mod h1:Y/kS3loxTEhy8Vill513EtPXr+CRDdae+Xj2BXXMy/c=
go.mau.fi/util v0.9.3 h1:aqNF8KDIN8bFpFbybSk+mEBil7IHeBwlujfyTnvP0uU=
go.mau.fi/util v0.9.3/go.mod h1:krWWfBM1jWTb5f8NCa2TLqWMQuM81X7TGQjhMjBeXmQ=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo=
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -569,38 +391,21 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -619,15 +424,10 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -638,12 +438,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -658,27 +454,19 @@ golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -690,12 +478,10 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -707,13 +493,8 @@ golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -744,13 +525,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -762,17 +538,10 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
maunium.net/go/mautrix v0.21.1 h1:Z+e448jtlY977iC1kokNJTH5kg2WmDpcQCqn+v9oZOA=
maunium.net/go/mautrix v0.21.1/go.mod h1:7F/S6XAdyc/6DW+Q7xyFXRSPb6IjfqMb1OMepQ8C8OE=
maunium.net/go/mautrix v0.24.2 h1:+AVT5kbcA/QuT5svrJKp4ivwoUmz+RRplMp3DnfpheI=
maunium.net/go/mautrix v0.24.2/go.mod h1:1ut900w++eE9by9yqCR2dQdMqwsHwZG5L+1bKB1EvSA=
maunium.net/go/mautrix v0.26.0 h1:valc2VmZF+oIY4bMq4Cd5H9cEKMRe8eP4FM7iiaYLxI=
maunium.net/go/mautrix v0.26.0/go.mod h1:NWMv+243NX/gDrLofJ2nNXJPrG8vzoM+WUCWph85S6Q=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@@ -31,6 +31,7 @@ func (app *appContext) clearEmails() {
func (app *appContext) clearDiscord() {
app.debug.Println(lm.HousekeepingDiscord)
discordUsers := app.storage.GetDiscord()
removeRoleOnDisable := app.config.Section("discord").Key("disable_enable_role").MustBool(false)
for _, discordUser := range discordUsers {
user, err := app.jf.UserByID(discordUser.JellyfinID, false)
// Make sure the user doesn't exist, and no other error has occured
@@ -40,7 +41,7 @@ func (app *appContext) clearDiscord() {
app.discord.RemoveRole(discordUser.MethodID().(string))
app.storage.DeleteDiscordKey(discordUser.JellyfinID)
default:
if user.Policy.IsDisabled {
if removeRoleOnDisable && user.Policy.IsDisabled {
app.discord.RemoveRole(discordUser.MethodID().(string))
}
continue

View File

@@ -1,8 +1,8 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<html lang="{{ .shortLang }}" dir="{{ .pageDirection }}" class="{{ .cssClass }}">
<head>
<title>404 - jfa-go</title>
{{ template "header.html" . }}
{{ template "header.txt" . }}
</head>
<body class="section">
<div class="page-container m-2 lg:my-20 lg:mx-64">

View File

@@ -1,16 +1,16 @@
{{ if .discordEnabled }}
<div id="modal-discord" class="modal">
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
<span class="heading mb-4">{{ .strings.linkDiscord }}</span>
<p class="content mb-4"> {{ .discordSendPINMessage }}</p>
<h1 class="text-center text-2xl mb-2 pin"></h1>
<div class="row center">
<a class="my-5 hover:underline">
<span class="mr-2">{{ .strings.joinTheServer }}</span>
<span id="discord-invite"></span>
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 flex flex-col gap-4">
<span class="heading">{{ .strings.linkDiscord }}</span>
<p class="content"> {{ .discordSendPINMessage }}</p>
<h1 class="text-center text-2xl pin"></h1>
<div class="flex flex-row gap-2 justify-center items-center">
<a class="hover:underline flex flex-row gap-4 items-center">
<span>{{ .strings.joinTheServer }}</span>
<span id="discord-invite" class="flex flex-row gap-2 items-center"></span>
</a>
</div>
<span class="button ~info @low full-width center mt-4" id="discord-waiting">{{ .strings.success }}</span>
<span class="button ~info @low full-width center" id="discord-waiting">{{ .strings.success }}</span>
</div>
</div>
{{ end }}

View File

@@ -1,18 +1,18 @@
{{ if .matrixEnabled }}
<div id="modal-matrix" class="modal">
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
<span class="heading mb-4">{{ .strings.linkMatrix }}</span>
<p class="content mb-4"> {{ .strings.matrixEnterUser }}</p>
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 flex flex-col gap-4">
<span class="heading">{{ .strings.linkMatrix }}</span>
<p class="content"> {{ .strings.matrixEnterUser }}</p>
<input type="text" class="input ~neutral @high" placeholder="@user:riot.im" id="matrix-userid">
<div class="subheading link-center mt-4">
<span class="shield ~info mr-4">
<div class="subheading flex flex-row gap-2 justify-center items-center">
<span class="shield ~info">
<span class="icon">
<i class="ri-chat-3-line"></i>
</span>
</span>
{{ .matrixUser }}
<span>{{ .matrixUser }}</span>
</div>
<span class="button ~info @low full-width center mt-4" id="matrix-send">{{ .strings.submit }}</span>
<span class="button ~info @low full-width center" id="matrix-send">{{ .strings.submit }}</span>
</div>
</div>
{{ end }}

View File

@@ -1,18 +1,18 @@
{{ if .telegramEnabled }}
<div id="modal-telegram" class="modal">
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
<span class="heading mb-4">{{ .strings.linkTelegram }}</span>
<p class="content mb-4">{{ .strings.sendPIN }}</p>
<p class="text-center text-2xl mb-2 pin"></p>
<a class="subheading link link-center" href="{{ .telegramURL }}" target="_blank">
<span class="shield ~info mr-4">
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 flex flex-col gap-4">
<span class="heading">{{ .strings.linkTelegram }}</span>
<p class="content">{{ .strings.sendPIN }}</p>
<p class="text-center text-2xl pin"></p>
<a class="subheading link flex flex-row gap-2 justify-center items-center" href="{{ .telegramURL }}" target="_blank">
<span class="shield ~info">
<span class="icon">
<i class="ri-telegram-line"></i>
</span>
</span>
&#64;<span class="username">{{ .telegramUsername }}</span>
<span class="hover:underline">&#64;<span class="username">{{ .telegramUsername }}</span></span>
</a>
<span class="button ~info @low full-width center mt-4" id="telegram-waiting">{{ .strings.success }}</span>
<span class="button ~info @low full-width center" id="telegram-waiting">{{ .strings.success }}</span>
</div>
</div>
{{ end }}

View File

@@ -0,0 +1,24 @@
<thead id="accounts-table-header">
<tr>
<th><input type="checkbox" value="" id="accounts-select-all"></th>
<th class="table-inline my-2 grid gap-4 place-items-stretch accounts-header-username">{{ .strings.username }}</th>
{{ if .jellyfinLogin }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-access-jfa">{{ .strings.accessJFA }}</th>
{{ end }}
<th class="grid gap-4 place-items-stretch accounts-header-email">{{ .strings.emailAddress }}</th>
{{ if .telegramEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-telegram">Telegram</th>
{{ end }}
{{ if .matrixEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-matrix">Matrix</th>
{{ end }}
{{ if .discordEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-discord">Discord</th>
{{ end }}
{{ if .referralsEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-referrals">{{ .strings.referrals }}</th>
{{ end }}
<th class="grid gap-4 place-items-stretch accounts-header-expiry">{{ .strings.expiry }}</th>
<th class="grid gap-4 place-items-stretch accounts-header-last-active">{{ .strings.lastActiveTime }}</th>
</tr>
</thead>

View File

@@ -1,6 +1,9 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<html lang="{{ .shortLang }}" dir="{{ .pageDirection }}" class="{{ .cssClass }}">
<head>
{{ template "syntaxhighlighting.txt" . }}
<title>Admin - jfa-go</title>
{{ template "header.txt" . }}
<script>
window.usernameEnabled = {{ .username }};
window.langFile = JSON.parse({{ .language }});
@@ -11,10 +14,8 @@
window.jfAllowAll = {{ .jfAllowAll }};
window.loginAppearance = "{{ .loginAppearance }}";
</script>
<title>Admin - jfa-go</title>
{{ template "header.html" . }}
</head>
<body class="max-w-full overflow-x-hidden section">
<body class="max-w-full section"><div class="overflow-x-hidden relative"><!-- for whatever reason position:relative stops hidden x overflow on ios and samsung web -->
{{ template "login-modal.html" . }}
<div id="modal-add-user" class="modal">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-2" id="form-add-user" href="">
@@ -44,50 +45,48 @@
<p>{{ .strings.buildTime }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .buildTime }}</span></p>
<p>{{ .strings.builtBy }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .builtBy }}</span></p>
<p>{{ .strings.buildTags }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .buildTags }}</span></p>
<div class="flex flex-row flex-wrap gap-2 my-2">
<a class="button ~neutral lang-link" href="https://github.com/hrfee/jfa-go"><i class="ri-github-line mr-2"></i>github</a>
<div class="flex flex-row flex-wrap gap-2">
<a class="button ~neutral lang-link flex flex-row gap-2" href="https://github.com/hrfee/jfa-go"><i class="ri-github-line"></i>github</a>
<a class="button ~urge lang-link" href="https://wiki.jfa-go.com">wiki/docs</a>
<a class="button ~positive lang-link" href="https://weblate.jfa-go.com">translation</a>
<div class="dropdown" tabindex="0">
<a href="https://github.com/sponsors/hrfee" target="_blank" class="button ~info dropdown-button lang-link">
<i class="ri-hand-heart-line mr-2"></i>
<a href="https://github.com/sponsors/hrfee" target="_blank" class="button ~info dropdown-button lang-link flex flex-row gap-2">
<i class="ri-hand-heart-line"></i>
donate
<span class="ml-2 chev"></span>
<span class="chev"></span>
</a>
<div class="dropdown-display">
<div class="card ~neutral @low">
<a href="https://github.com/sponsors/hrfee" target="_blank" class="button ~neutral mb-2 w-full lang-link">GitHub</a>
<a href="https://ko-fi.com/hrfee" target="_blank" class="button ~neutral mb-2 w-full lang-link">Ko-fi</a>
<div class="card ~neutral @low flex flex-col gap-2">
<a href="https://github.com/sponsors/hrfee" target="_blank" class="button ~neutral w-full lang-link">GitHub</a>
<a href="https://ko-fi.com/hrfee" target="_blank" class="button ~neutral w-full lang-link">Ko-fi</a>
</div>
</div>
</div>
<a class="button ~urge @low discord lang-link" href="https://discord.com/invite/MrtvuQmyhP" target="_blank"><i class="ri-discord-line mr-2"></i>discord</a>
<a class="button ~urge @low discord lang-link flex flex-row gap-2" href="https://discord.com/invite/MrtvuQmyhP" target="_blank"><i class="ri-discord-line"></i>discord</a>
</div>
<p><a href="https://github.com/hrfee/jfa-go/blob/main/LICENSE">Available under the MIT License. Font "Hanken Grotesk" available under SIL OFL 1.1 License.</a></p>
<pre class="font-mono bg-inherit">{{ .license }}</pre>
<pre class="font-mono bg-inherit force-ltr">{{ .license }}</pre>
</div>
</div>
<div id="modal-logs" class="modal">
<div class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content content card">
<div class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card">
<span class="heading">{{ .strings.logs }}<span class="modal-close">&times;</span></span>
<pre class="monospace" id="log-area"></pre>
<pre class="monospace force-ltr" id="log-area"></pre>
</div>
</div>
<div id="modal-tasks" class="modal">
<div class="relative mx-auto my-[10%] w-min card flex flex-col gap-2">
<h1 class="heading">{{ .strings.tasks }}<span class="modal-close">&times;</span></h1>
<p class="content">{{ .strings.tasksDescription }}</p>
<div id="modal-tasks-list" class="flex flex-col gap-2"></div>
</div>
</div>
<div id="modal-modify-user" class="modal">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-modify-user" href="">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-2" id="form-modify-user" href="">
<span class="heading"><span id="header-modify-user"></span> <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.modifySettingsDescription }}</p>
<div class="flex flex-col gap-4 my-2">
<div class="flex flex-row gap-2">
<label class="grow">
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-profile" checked>
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
</label>
<label class="grow">
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-user">
<span class="button ~neutral @low supra full-width center">{{ .strings.user }}</span>
</label>
</div>
<p class="content">{{ .strings.modifySettingsDescription }}</p>
<div class="flex flex-col gap-4">
<div id="modify-user-profile-source"></div>
<div class="select ~neutral @low">
<select id="modify-user-profiles"></select>
</div>
@@ -102,14 +101,18 @@
<input type="checkbox" id="modify-user-homescreen" checked>
<span>{{ .strings.applyHomescreenLayout }}</span>
</label>
<label class="switch">
<input type="checkbox" id="modify-user-ombi" checked>
<span>{{ .strings.applyOmbi }}</span>
</label>
<label class="switch">
<input type="checkbox" id="modify-user-jellyseerr" checked>
<span>{{ .strings.applyJellyseerr }}</span>
</label>
{{ if .ombiEnabled }}
<label class="switch">
<input type="checkbox" id="modify-user-ombi" checked>
<span>{{ .strings.applyOmbi }}</span>
</label>
{{ end }}
{{ if .jellyseerrEnabled }}
<label class="switch">
<input type="checkbox" id="modify-user-jellyseerr" checked>
<span>{{ .strings.applyJellyseerr }}</span>
</label>
{{ end }}
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.apply }}</span>
@@ -119,29 +122,20 @@
</div>
{{ if .referralsEnabled }}
<div id="modal-enable-referrals-user" class="modal">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-enable-referrals-user" href="">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-4" id="form-enable-referrals-user" href="">
<span class="heading"><span id="header-enable-referrals-user"></span> <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.enableReferralsDescription }}</p>
<div class="flex flex-row mb-4">
<label class="grow mr-2">
<input type="radio" name="enable-referrals-user-source" class="unfocused" id="radio-referrals-use-profile" checked>
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
</label>
<label class="grow ml-2">
<input type="radio" name="enable-referrals-user-source" class="unfocused" id="radio-referrals-use-invite">
<span class="button ~neutral @low supra full-width center">{{ .strings.invite }}</span>
</label>
</div>
<div class="select ~neutral @low mb-4">
<p class="content">{{ .strings.enableReferralsDescription }}</p>
<div id="enable-referrals-user-source"></div>
<div class="select ~neutral @low">
<select id="enable-referrals-user-profiles"></select>
</div>
<div class="select ~neutral @low mb-4 unfocused">
<div class="select ~neutral @low unfocused">
<select id="enable-referrals-user-invites"></select>
</div>
<label class="switch mb-4">
<label class="switch">
<input type="checkbox" id="enable-referrals-user-expiry">
<span>{{ .strings.useInviteExpiry }}</span>
<span class="flex flex-row support mt-2">{{ .strings.useInviteExpiryNote }}</span>
<span class="flex flex-row support">{{ .strings.useInviteExpiryNote }}</span>
</label>
<label>
<input type="submit" class="unfocused">
@@ -150,17 +144,19 @@
</form>
</div>
<div id="modal-enable-referrals-profile" class="modal">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-enable-referrals-profile" href="">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-4" id="form-enable-referrals-profile" href="">
<span class="heading"><span id="header-enable-referrals-profile">{{ .strings.enableReferrals }}</span> <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.enableReferralsProfileDescription }}</p>
<label class="supra" for="enable-referrals-profile-invites">{{ .strings.invite }}</label>
<div class="select ~neutral @low mb-4 mt-2">
<select id="enable-referrals-profile-invites"></select>
<p class="content">{{ .strings.enableReferralsProfileDescription }}</p>
<div class="flex flex-col gap-2">
<label class="supra" for="enable-referrals-profile-invites">{{ .strings.invite }}</label>
<div class="select ~neutral @low">
<select id="enable-referrals-profile-invites"></select>
</div>
</div>
<label class="switch mb-4">
<label class="switch flex flex-row gap-2">
<input type="checkbox" id="enable-referrals-profile-expiry">
<span>{{ .strings.useInviteExpiry }}</span>
<span class="flex flex-row support mt-2">{{ .strings.useInviteExpiryNote }}</span>
<span class="flex flex-row support">{{ .strings.useInviteExpiryNote }}</span>
</label>
<label>
<input type="submit" class="unfocused">
@@ -170,14 +166,14 @@
</div>
{{ end }}
<div id="modal-delete-user" class="modal">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-delete-user" href="">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-4" id="form-delete-user" href="">
<span class="heading"><span id="header-delete-user"></span> <span class="modal-close">&times;</span></span>
<div class="content mt-8">
<label class="switch mb-4">
<div class="content">
<label class="switch">
<input type="checkbox" id="delete-user-notify" checked>
<span>{{ .strings.sendDeleteNotificationEmail }}</span>
</label>
<textarea id="textarea-delete-user" class="textarea full-width ~neutral @low mb-4" placeholder="{{ .strings.sendDeleteNotificationExample }}"></textarea>
<textarea id="textarea-delete-user" class="textarea full-width ~neutral @low" placeholder="{{ .strings.sendDeleteNotificationExample }}"></textarea>
<label>
<input type="submit" class="unfocused">
<span class="button ~critical @low full-width center supra submit">{{ .strings.delete }}</span>
@@ -243,7 +239,7 @@
<input type="checkbox" id="expiry-extend-enable" checked>
<span>{{ .strings.sendDeleteNotificationEmail }}</span>
</label>
<textarea id="textarea-extend-enable" class="textarea full-width ~neutral @low mb-4" placeholder="{{ .strings.sendDeleteNotificationExample }}"></textarea>
<textarea id="textarea-extend-enable" class="textarea full-width ~neutral @low" placeholder="{{ .strings.sendDeleteNotificationExample }}"></textarea>
<label>
<input type="submit" class="unfocused">
<span class="button ~critical @low full-width center supra submit">{{ .strings.submit }}</span>
@@ -254,21 +250,23 @@
<div id="modal-announce" class="modal">
<form class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card" id="form-announce" href="">
<span class="heading"><span id="header-announce"></span> <span class="modal-close">&times;</span></span>
<div class="flex flex-col md:flex-row">
<div class="col card ~neutral @low">
<div id="announce-details">
<div class="flex flex-row flex-wrap gap-4">
<div class="card ~neutral @low flex flex-col gap-2 justify-between basis-[24rem] grow-[4]">
<div id="announce-details" class="flex flex-col gap-2">
<span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span>
<div id="announce-variables">
<span class="button ~urge @low mb-2 mt-4" id="announce-variables-username" style="margin-left: 0.25rem; margin-right: 0.25rem;"><span class="font-mono bg-inherit">{username}</span></span>
<div id="announce-variables" class="flex flex-row flex-wrap gap-2">
<span class="button ~urge @low" id="announce-variables-username"><span class="font-mono bg-inherit">{username}</span></span>
</div>
<label class="label supra" for="announce-subject"> {{ .strings.subject }}</label>
<input type="text" id="announce-subject" class="input ~neutral @low mb-2 mt-4">
<input type="text" id="announce-subject" class="input ~neutral @low">
<label class="label supra" for="textarea-announce">{{ .strings.message }}</label>
<textarea id="textarea-announce" class="textarea full-width ~neutral @low mt-4 font-mono"></textarea>
<p class="support mt-4 mb-2">{{ .strings.markdownSupported }}</p>
<textarea id="textarea-announce" class="textarea full-width ~neutral @low font-mono"></textarea>
<p class="support">{{ .strings.markdownSupported }}</p>
<p class="support editor-syntax-description">{{ .strings.syntaxDescription }}</p>
</div>
<label class="label unfocused" id="announce-name"><p class="supra">{{ .strings.name }}</p>
<input type="text" class="input ~neutral @low mb-2 mt-4">
<input type="text" class="input ~neutral @low">
<p class="support">{{ .strings.templateEnterName }}</p>
</label>
<div class="flex flex-row justify-between">
@@ -279,17 +277,17 @@
<span class="button ~info @low center supra" id="save-announce">{{ .strings.saveAsTemplate }}</span>
</div>
</div>
<div class="col card ~neutral @low">
<div class="card ~neutral @low flex flex-col gap-2 basis-[24rem] grow">
<span class="subheading supra">{{ .strings.preview }}</span>
<div class="mt-8" id="announce-preview"></div>
<div id="announce-preview"></div>
</div>
</div>
</form>
</div>
<div id="modal-customize" class="modal">
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3">
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-2">
<span class="heading">{{ .strings.customizeMessages }} <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.customizeMessagesDescription }}</p>
<p class="content">{{ .strings.customizeMessagesDescription }}</p>
<div class="">
<table class="table">
<thead>
@@ -307,8 +305,8 @@
<div id="modal-editor" class="modal">
<form class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card" id="form-editor" href="">
<span class="heading"><span id="header-editor"></span> <span class="modal-close">&times;</span></span>
<div class="row">
<div class="col card ~neutral @low flex flex-col gap-2 justify-between">
<div class="flex flex-row flex-wrap gap-4">
<div class="card ~neutral @low flex flex-col gap-2 justify-between basis-[24rem] grow-[4]">
<div class="flex flex-col gap-2">
<aside class="aside sm ~urge dark:~d_info @low" id="aside-editor"></aside>
<label class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</label>
@@ -317,16 +315,17 @@
<div id="editor-conditionals"></div>
<label class="label supra" for="textarea-editor">{{ .strings.message }}</label>
<textarea id="textarea-editor" class="textarea full-width flex-auto ~neutral @low font-mono"></textarea>
<p class="support">{{ .strings.markdownSupported }}</p>
<p class="support editor-syntax-description">{{ .strings.syntaxDescription }}</p>
</div>
<div class="flex flex-col gap-2">
<p class="support">{{ .strings.markdownSupported }}</p>
<label class="w-full">
<input type="submit" class="unfocused">
<span class="button ~urge @low w-full supra submit">{{ .strings.submit }}</span>
</label>
</div>
</div>
<div class="col card ~neutral @low flex flex-col gap-2">
<div class="card ~neutral @low flex flex-col gap-2 basis-[24rem] grow">
<span class="subheading supra">{{ .strings.preview }}</span>
<div id="editor-preview"></div>
</div>
@@ -334,19 +333,19 @@
</form>
</div>
<div id="modal-restart" class="modal">
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~critical @low">
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~critical @low flex flex-col gap-4">
<span class="heading">{{ .strings.settingsRestartRequired }} <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.settingsRestartRequiredDescription }}</p>
<div class="float-right">
<span class="button ~info @low mb-2" id="settings-apply-no-restart">{{ .strings.settingsApplyRestartLater }}</span>
<p class="content">{{ .strings.settingsRestartRequiredDescription }}</p>
<div class="flex flex-row justify-end gap-2">
<span class="button ~info @low" id="settings-apply-no-restart">{{ .strings.settingsApplyRestartLater }}</span>
<span class="button ~critical @low" id="settings-apply-restart">{{ .strings.settingsApplyRestartNow }}</span>
</div>
</div>
</div>
<div id="modal-backups" class="modal">
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-2/3">
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-2/3 flex flex-col gap-4">
<span class="heading">{{ .strings.backups }} <span class="modal-close">&times;</span></span>
<div class="content my-4">
<div class="content">
{{ .strings.backupsDescription }}
<ul>
<li>{{ .strings.backupsCopy }}</li>
@@ -354,11 +353,11 @@
<li><a target="_blank" href="https://wiki.jfa-go.com/docs/backups/">{{ .strings.wikiPage }}</a></li>
</ul>
</div>
<div class="flex flex-row flex-wrap my-2">
<button class="button ~info @low mr-2 mb-2" id="settings-backups-backup">{{ .strings.backupNow }}</button>
<button class="button ~neutral @low mr-2 mb-2" id="settings-backups-upload">{{ .strings.backupUpload }}</button>
<div class="flex flex-row flex-wrap gap-2">
<button class="button ~info @low" id="settings-backups-backup">{{ .strings.backupNow }}</button>
<button class="button ~neutral @low" id="settings-backups-upload">{{ .strings.backupUpload }}</button>
<input id="backups-file" name="backups-file" type="file" hidden>
<button class="button ~neutral @low mr-2 mb-2" id="settings-backups-sort-direction">{{ .strings.sortDirection }}</button>
<button class="button ~neutral @low flex flex-row gap-2" id="settings-backups-sort-direction">{{ .strings.sortDirection }}</button>
</div>
<div class="overflow-x-auto text-xs md:text-sm">
<table class="table">
@@ -376,12 +375,14 @@
</div>
</div>
<div id="modal-backed-up" class="modal">
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~neutral @low">
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~neutral @low flex flex-col gap-4">
<span class="heading">{{ .strings.backupCreated }} <span class="modal-close">&times;</span></span>
<p class="content my-4" id="settings-backed-up-location"></p>
<p class="content my-4">{{ .strings.backupCanDownload }}</p>
<div class="flex flex-col gap-2">
<p class="content" id="settings-backed-up-location"></p>
<p class="content">{{ .strings.backupCanDownload }}</p>
</div>
<div>
<button class="button flex w-full ~info @low mb-2"><span class="flex items-center" id="settings-backed-up-download">{{ .strings.download }}</span></button>
<button class="button flex w-full ~info @low"><span class="flex flex-row gap-2 items-center" id="settings-backed-up-download">{{ .strings.download }}</span></button>
</div>
</div>
</div>
@@ -392,17 +393,17 @@
</div>
</div>
<div id="modal-send-pwr" class="modal">
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~neutral @low">
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~neutral @low flex flex-col gap-4">
<span class="heading">{{ .strings.sendPWR }}</span>
<p class="content my-2" id="send-pwr-note"></p>
<span class="button ~urge @low mt-2" id="send-pwr-link">{{ .strings.copy }}</span>
<p class="content" id="send-pwr-note"></p>
<span class="button ~urge @low" id="send-pwr-link">{{ .strings.copy }}</span>
</div>
</div>
<div id="modal-ombi-profile" class="modal">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-ombi-defaults" href="">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-4" id="form-ombi-defaults" href="">
<span class="heading">{{ .strings.ombiProfile }} <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.ombiUserDefaultsDescription }}</p>
<div class="select ~neutral @low mb-4">
<p class="content">{{ .strings.ombiUserDefaultsDescription }}</p>
<div class="select ~neutral @low">
<select></select>
</div>
<label>
@@ -412,10 +413,10 @@
</form>
</div>
<div id="modal-jellyseerr-profile" class="modal">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-jellyseerr-defaults" href="">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-4" id="form-jellyseerr-defaults" href="">
<span class="heading">{{ .strings.jellyseerrProfile }} <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.jellyseerrUserDefaultsDescription }}</p>
<div class="select ~neutral @low mb-4">
<p class="content">{{ .strings.jellyseerrUserDefaultsDescription }}</p>
<div class="select ~neutral @low">
<select></select>
</div>
<label>
@@ -425,9 +426,9 @@
</form>
</div>
<div id="modal-user-profiles" class="modal">
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-2/3 content card">
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-2/3 card flex flex-col gap-4">
<span class="heading">{{ .strings.userProfiles }} <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.userProfilesDescription }}</p>
<p class="content">{{ .strings.userProfilesDescription }}</p>
<div class="table-responsive">
<table class="table">
<thead>
@@ -445,6 +446,7 @@
{{ end }}
<th>{{ .strings.from }}</th>
<th>{{ .strings.userProfilesLibraries }}</th>
<th></th>
<th><span class="button ~neutral @high" id="button-profile-create">{{ .strings.create }}</span></th>
</tr>
</thead>
@@ -453,51 +455,69 @@
</div>
</div>
</div>
<div id="modal-edit-profile" class="modal">
<form class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-2/3 card flex flex-col gap-2" id="form-edit-profile">
<span class="heading">{{ .strings.editProfile }} <span class="modal-close">&times;</span></span>
<p class="content">{{ .strings.editProfileDescription }}</p>
<div id="modal-edit-profile-editor"></div>
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.submit }}</span>
</label>
</form>
</div>
<div id="modal-add-profile" class="modal">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-add-profile" href="">
<span class="heading">{{ .strings.addProfile }} <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.addProfileDescription }}</p>
<label>
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-2" id="form-add-profile" href="">
<h1 class="heading">{{ .strings.addProfile }} <span class="modal-close">&times;</span></h1>
<p class="content">{{ .strings.addProfileDescription }}</p>
<label class="flex flex-col gap-2">
<span class="supra">{{ .strings.addProfileNameOf }} </span>
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.name }}" id="add-profile-name">
<label>
<input type="text" class="field input ~neutral @high" placeholder="{{ .strings.name }}" id="add-profile-name">
</label>
<label class="flex flex-col gap-2">
<span class="supra">{{ .strings.user }}</span>
<div class="select ~neutral @low mt-4 mb-2">
<div class="select ~neutral @low">
<select id="add-profile-user"></select>
</div>
</label>
<label class="switch mb-4">
<label class="switch">
<input type="checkbox" id="add-profile-homescreen" checked>
<span>{{ .strings.addProfileStoreHomescreenLayout }}</span>
</label>
{{ if .jellyseerrEnabled }}
<label class="switch">
<input type="checkbox" id="add-profile-jellyseerr" checked>
<span>{{ .strings.addProfileStoreJellyseerr }}</span>
</label>
{{ end }}
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.create }}</span>
<span class="button ~urge @low w-full center supra submit">{{ .strings.create }}</span>
</label>
</form>
</div>
<div id="modal-update" class="modal">
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 content card">
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 card flex flex-col gap-2">
<span class="heading">{{ .strings.updates }} <span class="modal-close">&times;</span></span>
<p class="content">
<h2 class="mt-2">
<div class="content flex flex-col">
<h2>
<a id="update-version"></a> (<span class="font-mono bg-inherit" id="update-commit"></span>)
</h2>
<p class="content mt-2" id="update-description"></p>
<p class="support mt-2" id="update-date"></p>
<div class="content markdown-box mt-2" id="update-changelog"></div>
</p>
<span class="button ~info @low full-width center mt-2" id="update-download">{{ .strings.download }}</span>
<span class="button ~urge @low full-width center mt-2" id="update-update">{{ .strings.update }}</span>
<p class="content" id="update-description"></p>
<div class="content markdown-box" id="update-changelog"></div>
<p class="support" id="update-date"></p>
</div>
<span class="button ~info @low full-width center" id="update-download">{{ .strings.download }}</span>
<span class="button ~urge @low full-width center" id="update-update">{{ .strings.update }}</span>
</div>
</div>
{{ template "account-linking-telegram.html" . }}
{{ if .discordEnabled }}
<div id="modal-discord" class="modal">
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3">
<span class="heading mb-4"><span id="discord-header"></span><span class="modal-close">&times;</span></span>
<p class="content mb-4" id="discord-description"></p>
<div class="row">
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-2">
<span class="heading"><span id="discord-header"></span><span class="modal-close">&times;</span></span>
<p class="content" id="discord-description"></p>
<div>
<input type="search" class="col sm field ~neutral @low input" id="discord-search" placeholder="user#1234">
</div>
<table class="table"><tbody id="discord-list"></tbody></table>
@@ -505,20 +525,29 @@
</div>
{{ end }}
<div id="modal-matrix" class="modal">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-matrix" href="">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-2" id="form-matrix" href="">
<span class="heading">{{ .strings.linkMatrix }}</span>
<p class="content my-4">{{ .strings.linkMatrixDescription }}</p>
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.matrixHomeServer }}" id="matrix-homeserver">
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="matrix-user">
<input type="password" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.password }}" id="matrix-password">
<p class="content">{{ .strings.linkMatrixDescription }}</p>
<label class="flex flex-col gap-2">
<span class="supra">{{ .strings.matrixHomeServer }}</span>
<input type="text" class="field input ~neutral @high" placeholder="{{ .strings.matrixHomeServer }}" id="matrix-homeserver">
</label>
<label class="flex flex-col gap-2">
<span class="supra">{{ .strings.username }}</span>
<input type="text" class="field input ~neutral @high" placeholder="{{ .strings.username }}" id="matrix-user">
</label>
<label class="flex flex-col gap-2">
<span class="supra">{{ .strings.password }}</span>
<input type="password" class="field input ~neutral @high" placeholder="{{ .strings.password }}" id="matrix-password">
</label>
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.submit }}</span>
</label>
</label>"
</form>
</div>
<div id="notification-box"></div>
<div class="page-container m-2 lg:my-20 lg:mx-64 flex flex-col gap-4">
<div class="page-container m-2 lg:my-20 lg:mx-64 flex flex-col gap-4 overflow-x-hidden">
<div class="top-2 inset-x-2 lg:absolute flex flex-row justify-between">
<div class="flex flex-row gap-2">
{{ template "lang-select.html" . }}
@@ -528,17 +557,17 @@
<span class="button ~critical @low unfocused" id="logout-button">{{ .strings.logout }}</span>
{{ if .userPageEnabled }}
<div class="">
<a class="button ~info" href="{{ .pages.Base }}{{ .pages.MyAccount }}/"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
<a class="button ~info flex flex-row gap-2" href="{{ .pages.Base }}{{ .pages.MyAccount }}/"><i class="ri-account-circle-fill"></i>{{ .strings.myAccount }}</a>
</div>
{{ end }}
</div>
</div>
<header>
<div class="flex flex-row overflow-x-auto items-center gap-2">
<span id="button-tab-invites" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.invites }}</span>
<span id="button-tab-accounts" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.accounts }}</span>
<span id="button-tab-activity" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.activity }}</span>
<span id="button-tab-settings" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.settings }}</span>
<div class="flex flex-row overflow-x-auto items-center gap-2 scroll-smooth">
<button type="button" id="button-tab-invites" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.invites }}</button>
<button type="button" id="button-tab-accounts" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.accounts }}</button>
<button type="button" id="button-tab-activity" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.activity }}</button>
<button type="button" id="button-tab-settings" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.settings }}</button>
</div>
</header>
<div id="tab-invites" class="flex flex-col gap-4">
@@ -550,16 +579,7 @@
<span class="heading">{{ .strings.create }}</span>
<div class="flex flex-col md:flex-row gap-3" id="create-inv">
<div class="card ~neutral @low flex flex-col gap-2 flex-1">
<div class="flex flex-row gap-2">
<label class="w-1/2">
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
<span class="button ~neutral @high supra full-width center">{{ .strings.inviteDuration }}</span>
</label>
<label class="w-1/2">
<input type="radio" name="duration" class="unfocused" id="radio-user-expiry">
<span class="button ~neutral @low supra full-width center">{{ .strings.userExpiry }}</span>
</label>
</div>
<div id="invites-duration-type-tabs"></div>
<div id="inv-duration" class="flex flex-col gap-2">
<div class="flex flex-row gap-2">
<div class="grow flex flex-col gap-4">
@@ -602,9 +622,9 @@
<div class="flex flex-row gap-2">
<p class="support">{{ .strings.userExpiryDescription }}</p>
<div>
<label for="create-user-expiry-enabled" class="button ~neutral @low">
<label for="create-user-expiry-enabled" class="button ~neutral @low flex flex-row gap-2">
<input type="checkbox" id="create-user-expiry-enabled" aria-label="User duration enabled">
<span class="ml-2">{{ .strings.enabled }} </span>
<span>{{ .strings.enabled }} </span>
</label>
</div>
</div>
@@ -678,20 +698,6 @@
</div>
</div>
<div id="create-send-to-container" class="flex flex-col gap-4">
<label class="label supra">{{ .strings.inviteSendToEmail }}</label>
<div class="flex flex-row gap-2">
{{ if .discordEnabled }}
<input type="text" id="create-send-to" class="input ~neutral @low" placeholder="example@example.com | user#1234">
<span id="create-send-to-search" class="button ~neutral @low">
<i class="icon ri-search-2-line" title="{{ .strings.search }}"></i>
</span>
{{ else }}
<input type="email" id="create-send-to" class="input ~neutral @low" placeholder="example@example.com">
{{ end }}
<label for="create-send-to-enabled" class="button ~neutral @low">
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
</label>
</div>
</div>
</div>
<div>
@@ -702,18 +708,18 @@
</div>
</div>
<div id="tab-accounts" class="flex flex-col gap-4 unfocused">
<div class="card @low dark:~d_neutral accounts mb-4 overflow-visible flex flex-col gap-2">
<div class="card @low dark:~d_neutral accounts overflow-visible flex flex-col gap-2">
<div id="accounts-filter-dropdown" class="dropdown manual z-10 w-full">
<div class="flex flex-col md:flex-row align-middle gap-2">
<div class="flex flex-row align-middle justify-between md:justify-normal">
<span class="text-3xl font-bold mr-4">{{ .strings.accounts }}</span>
<div class="flex flex-row gap-4 align-middle justify-between md:justify-normal">
<span class="text-3xl font-bold">{{ .strings.accounts }}</span>
<span class="dropdown-manual-toggle"><button class="h-full button ~neutral @low center" id="accounts-filter-button" tabindex="0">{{ .strings.filters }}</button></span>
</div>
<div class="flex flex-row align-middle w-full gap-2">
<input type="search" class="field ~neutral @low input search mr-2" id="accounts-search" placeholder="{{ .strings.search }}">
<input type="search" class="field ~neutral @low input search" id="accounts-search" placeholder="{{ .strings.search }}">
<span class="button ~neutral @low center inside-input rounded-s-none accounts-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
<div class="tooltip left">
<button class="button ~info @low center h-full accounts-search-server flex flex-row gap-1" aria-label="{{ .strings.searchAllRecords }}" text="{{ .strings.searchAllRecords }}">
<button class="button ~info @low center h-full accounts-search-server gap-1" aria-label="{{ .strings.searchAllRecords }}" text="{{ .strings.searchAllRecords }}">
<i class="ri-search-line"></i>
<span>{{ .strings.searchAll }}</span>
</button>
@@ -723,7 +729,7 @@
</div>
</div>
<div class="dropdown-display max-w-full">
<div class="card ~neutral @low mt-2 overflow-x-scroll" id="accounts-filter-list">
<div class="card ~neutral @low overflow-x-scroll" id="accounts-filter-list">
<p class="supra pb-2">{{ .strings.filters }}</p>
</div>
</div>
@@ -738,14 +744,15 @@
</div>
<div class="supra sm">{{ .strings.actions }}</div>
<div class="flex flex-row flex-wrap gap-3">
<button type="button" title="{{ .strings.back }}" class="button ~neutral @low inline-flex gap-1 unfocused" id="user-details-back" aria-label="{{ .strings.back }}"><i class="icon ri-arrow-left-fill"></i>{{ .strings.back }}</button>
<button class="button ~neutral @low center accounts-load-all">{{ .strings.loadAll }}</button>
<span class="button ~neutral @low center " id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
<div id="accounts-announce-dropdown" class="dropdown pb-0i " tabindex="0">
<span class="w-full button ~info @low center items-baseline" id="accounts-announce">{{ .strings.announce }}</span>
<span class="w-full button ~info @low center items-baseline flex flex-row gap-2" id="accounts-announce">{{ .strings.announce }}</span>
<div class="dropdown-display">
<div class="card ~neutral @low">
<span class="supra sm">{{ .strings.templates }}</span>
<div id="accounts-announce-templates"></div>
<div id="accounts-announce-templates" class="flex flex-col gap-2"></div>
</div>
</div>
</div>
@@ -754,11 +761,11 @@
<span class="button ~urge @low center " id="accounts-enable-referrals">{{ .strings.enableReferrals }}</span>
{{ end }}
<div id="accounts-expiry-dropdown" class="dropdown pb-0i " tabindex="0">
<span class="w-full button ~positive @low center items-baseline" id="accounts-expiry-dropdown-button">{{ .strings.expiry }} <i class="ri-arrow-down-s-line ml-2"></i></span>
<span class="w-full button ~positive @low center items-baseline flex flex-row gap-2" id="accounts-expiry-dropdown-button">{{ .strings.expiry }}<i class="ri-arrow-down-s-line"></i></span>
<div class="dropdown-display">
<div class="card ~neutral @low">
<div class="card ~neutral @low flex flex-col gap-2">
<span class="button ~warning full-width @low center" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
<span class="button ~critical full-width @low center mt-2" id="accounts-remove-expiry">{{ .strings.removeExpiry }}</span>
<span class="button ~critical full-width @low center" id="accounts-remove-expiry">{{ .strings.removeExpiry }}</span>
</div>
</div>
</div>
@@ -773,73 +780,56 @@
<span class="button ~info @low center unfocused " id="accounts-send-pwr">{{ .strings.sendPWR }}</span>
<span class="button ~critical @low center " id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
</div>
<div class="card @low accounts-header table-responsive">
<table class="table text-base leading-5">
<thead>
<tr>
<th><input type="checkbox" value="" id="accounts-select-all"></th>
<th class="table-inline my-2 grid gap-4 place-items-stretch accounts-header-username">{{ .strings.username }}</th>
{{ if .jellyfinLogin }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-access-jfa">{{ .strings.accessJFA }}</th>
{{ end }}
<th class="grid gap-4 place-items-stretch accounts-header-email">{{ .strings.emailAddress }}</th>
{{ if .telegramEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-telegram">Telegram</th>
{{ end }}
{{ if .matrixEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-matrix">Matrix</th>
{{ end }}
{{ if .discordEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-discord">Discord</th>
{{ end }}
{{ if .referralsEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-referrals">{{ .strings.referrals }}</th>
{{ end }}
<th class="grid gap-4 place-items-stretch accounts-header-expiry">{{ .strings.expiry }}</th>
<th class="grid gap-4 place-items-stretch accounts-header-last-active">{{ .strings.lastActiveTime }}</th>
</tr>
</thead>
<tbody id="accounts-list"></tbody>
</table>
<div id="accounts-loader"></div>
<div class="unfocused h-[100%] my-3" id="accounts-not-found">
<div class="flex flex-col gap-2 h-[100%] justify-center items-center">
<span class="text-2xl font-medium italic text-center">{{ .strings.noResultsFound }}</span>
<span class="text-sm font-light italic unfocused text-center" id="accounts-no-local-results">{{ .strings.noResultsFoundLocally }}</span>
<div class="flex flex-row">
<button class="button ~neutral @low accounts-search-clear flex flex-row gap-2">
<span>{{ .strings.clearSearch }}</span><i class="ri-close-line"></i>
</button>
<div id="accounts-table-details-container">
<div class="card @low accounts-details transition-opacity transition-display unfocused">
<div id="accounts-loader"></div>
</div>
<div class="card @low accounts-header overflow-x-scroll transition-opacity transition-display" id="accounts-table">
<table class="table text-base leading-5">
{{ template "accounts-table-header.txt" . }}
<tbody id="accounts-list"></tbody>
</table>
<div id="accounts-loader"></div>
<div class="unfocused h-[100%] my-3" id="accounts-not-found">
<div class="flex flex-col gap-2 h-[100%] justify-center items-center">
<span class="text-2xl font-medium italic text-center">{{ .strings.noResultsFound }}</span>
<span class="text-sm font-light italic unfocused text-center" id="accounts-no-local-results">{{ .strings.noResultsFoundLocally }}</span>
<div class="flex flex-row">
<button class="button ~neutral @low accounts-search-clear gap-1">
<i class="ri-close-line"></i>
<span>{{ .strings.clearSearch }}</span>
</button>
</div>
</div>
</div>
</div>
<div class="flex flex-row gap-2 justify-center">
<button class="button ~neutral @low" id="accounts-load-more">{{ .strings.loadMore }}</button>
<button class="button ~neutral @low accounts-load-all">{{ .strings.loadAll }}</button>
<button class="button ~info @low center accounts-search-server flex flex-row gap-1" aria-label="{{ .strings.searchAllRecords }}" text="{{ .strings.searchAllRecords }}">
<i class="ri-search-line"></i>
<span>{{ .strings.searchAllRecords }}</span>
</button>
<div class="flex flex-row gap-2 my-3 justify-center">
<button class="button ~neutral @low" id="accounts-load-more">{{ .strings.loadMore }}</button>
<button class="button ~neutral @low accounts-load-all">{{ .strings.loadAll }}</button>
<button class="button ~info @low center accounts-search-server gap-1" aria-label="{{ .strings.searchAllRecords }}" text="{{ .strings.searchAllRecords }}">
<i class="ri-search-line"></i>
<span>{{ .strings.searchAllRecords }}</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div id="tab-activity" class="flex flex-col gap-4 unfocused">
<div class="card @low dark:~d_neutral activity mb-4 overflow-visible flex flex-col gap-2">
<div class="card @low dark:~d_neutral activity overflow-visible flex flex-col gap-2">
<div id="activity-filter-dropdown" class="dropdown manual z-10 w-full" tabindex="0">
<div class="flex flex-col md:flex-row align-middle gap-2">
<div class="flex flex-row align-middle justify-between md:justify-normal">
<span class="text-3xl font-bold mr-4">{{ .strings.activity }}</span>
<div class="flex flex-row align-middle">
<div class="flex flex-row gap-4 align-middle justify-between md:justify-normal">
<span class="text-3xl font-bold">{{ .strings.activity }}</span>
<div class="flex flex-row gap-2 align-middle">
<span class="dropdown-manual-toggle"><button class="h-full button ~neutral @low center" id="activity-filter-button">{{ .strings.filters }}</button></span>
<button class="button ~neutral @low ml-2" id="activity-sort-direction">{{ .strings.sortDirection }}</button>
<button class="button ~neutral @low" id="activity-sort-direction">{{ .strings.sortDirection }}</button>
</div>
</div>
<div class="flex flex-row align-middle w-full gap-2">
<input type="search" class="field ~neutral @low input search mr-2" id="activity-search" placeholder="{{ .strings.search }}">
<input type="search" class="field ~neutral @low input search" id="activity-search" placeholder="{{ .strings.search }}">
<span class="button ~neutral @low center inside-input rounded-s-none activity-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
<div class="tooltip left">
<button class="button ~info @low center h-full activity-search-server flex flex-row gap-1" aria-label="{{ .strings.searchAllRecords }}" text="{{ .strings.searchAllRecords }}">
<button class="button ~info @low center h-full activity-search-server gap-1" aria-label="{{ .strings.searchAllRecords }}" text="{{ .strings.searchAllRecords }}">
<i class="ri-search-line"></i>
<span>{{ .strings.searchAll }}</span>
</button>
@@ -849,7 +839,7 @@
</div>
</div>
<div class="dropdown-display max-w-full">
<div class="card ~neutral @low mt-2 overflow-x-scroll" id="activity-filter-list">
<div class="card ~neutral @low overflow-x-scroll" id="activity-filter-list">
<p class="supra pb-2">{{ .strings.filters }}</p>
</div>
</div>
@@ -866,19 +856,20 @@
<span class="text-2xl font-medium italic text-center">{{ .strings.noResultsFound }}</span>
<span class="text-sm font-light italic unfocused text-center" id="activity-no-local-results">{{ .strings.noResultsFoundLocally }}</span>
<div class="flex flex-row">
<button class="button ~neutral @low activity-search-clear flex flex-row gap-2">
<span>{{ .strings.clearSearch }}</span><i class="ri-close-line"></i>
<button class="button ~neutral @low activity-search-clear gap-1">
<i class="ri-close-line"></i>
<span>{{ .strings.clearSearch }}</span>
</button>
<button class="button ~neutral @low unfocused" id="activity-keep-searching">{{ .strings.keepSearching }}</button>
</div>
</div>
</div>
<div id="activity-card-list"></div>
<div id="activity-card-list" class="flex flex-col gap-2"></div>
<div id="activity-loader"></div>
<div class="flex flex-row gap-2 justify-center">
<button class="button ~neutral @low" id="activity-load-more">{{ .strings.loadMore }}</button>
<button class="button ~neutral @low activity-load-all">{{ .strings.loadAll }}</button>
<button class="button ~info @low center activity-search-server flex flex-row gap-1" aria-label="{{ .strings.searchAllRecords }}" text="{{ .strings.searchAllRecords }}">
<button class="button ~info @low center activity-search-server gap-1" aria-label="{{ .strings.searchAllRecords }}" text="{{ .strings.searchAllRecords }}">
<i class="ri-search-line"></i>
<span>{{ .strings.searchAllRecords }}</span>
</button>
@@ -888,40 +879,41 @@
<div id="tab-settings" class="flex flex-col gap-4 unfocused">
<div class="card @low dark:~d_neutral settings overflow flex flex-col gap-2">
<div class="flex flex-col md:flex-row align-middle gap-2">
<div class="flex flex-row align-middle justify-between md:justify-normal">
<div class="flex flex-row align-middle justify-between md:justify-normal gap-2">
<span class="heading">{{ .strings.settings }}</span>
<label for="settings-advanced-enabled" class="button ~neutral @low ml-2">
<label for="settings-advanced-enabled" class="button ~neutral @low flex flex-row gap-2">
<input type="checkbox" id="settings-advanced-enabled" aria-label="Advanced settings enabled">
<span class="ml-2">{{ .strings.advancedSettings }} </span>
<span>{{ .strings.advancedSettings }} </span>
</label>
</div>
<div class="flex flex-row justify-start md:justify-end gap-2 w-full">
<span class="button ~neutral @low gap-1 unfocused" id="settings-tasks"><i class="ri-calendar-schedule-line"></i>{{ .strings.tasks }}</span>
<span class="button ~neutral @low" id="settings-logs">{{ .strings.logs }}</span>
<span class="button ~info @low" id="settings-backups">{{ .strings.backups }}</span>
<span class="button ~neutral @low" id="settings-restart">{{ .strings.settingsRestart }}</span>
<span class="button ~urge @low unfocused" id="settings-save">{{ .strings.settingsSave }}</span>
<span class="button ~info @low gap-1" id="settings-backups"><i class="icon ri-file-copy-line"></i>{{ .strings.backups }}</span>
<span class="button ~neutral @low gap-1" id="settings-restart"><i class="icon ri-restart-line"></i>{{ .strings.settingsRestart }}</span>
<span class="button ~urge @low unfocused gap-1" id="settings-save"><i class="icon ri-save-line"></i>{{ .strings.settingsSave }}</span>
</div>
</div>
<div class="flex flex-col md:flex-row gap-3">
<div class="flex flex-col md:flex-row gap-3 force-ltr">
<div class="@low dark:~d_neutral flex md:flex flex-col gap-2" id="settings-sidebar">
<div class="flex flex-row justify-between">
<input type="search" class="field ~neutral @low input settings-section-button justify-between" id="settings-search" placeholder="{{ .strings.search }}">
<button class="button ~neutral @low center -ml-10 rounded-s-none settings-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></button>
<button class="button ~neutral @low center inside-input rounded-s-none settings-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></button>
</div>
<div id="settings-loader" class="flex flex-row flex-wrap gap-2">
<span class="button ~neutral @low justify-center grow" id="setting-about"><span class="flex">{{ .strings.aboutProgram }} <i class="ri-information-line ml-2"></i></span></span>
<a class="button ~urge dark:~d_info @low justify-center grow" target="_blank" href="https://wiki.jfa-go.com"><span class="flex">{{ .strings.wiki }} <i class="ri-book-shelf-line ml-2"></i></a>
<span class="button ~neutral @low justify-center grow" id="setting-profiles"><span class="flex">{{ .strings.userProfiles }} <i class="ri-user-line ml-2"></i></span></span>
<span class="button ~neutral @low justify-center grow flex flex-row gap-2" id="setting-about"><span class="flex flex-row gap-2">{{ .strings.aboutProgram }} <i class="ri-information-line"></i></span></span>
<a class="button ~urge dark:~d_info @low justify-center grow flex flex-row gap-2" target="_blank" href="https://wiki.jfa-go.com"><span class="flex flex-row gap-2">{{ .strings.wiki }} <i class="ri-book-shelf-line"></i></a>
<span class="button ~neutral @low justify-center grow flex flex-row gap-2" id="setting-profiles"><span class="flex flex-row gap-2">{{ .strings.userProfiles }} <i class="ri-user-line"></i></span></span>
</div>
<div class="flex md:flex flex-col gap-2 overflow-y-scroll" id="settings-sidebar-items"></div>
</div>
<div class="card ~neutral @low overflow flex-1 grow" id="settings-panel">
<div class="settings-section unfocused h-[100%]" id="settings-not-found">
<div class="flex flex-col h-[100%] justify-center items-center">
<span class="text-2xl font-medium italic mb-2">{{ .strings.noResultsFound }}</span>
<span class="mb-2 px-12 text-center">{{ .strings.settingsMaybeUnderAdvanced }}</span>
<button class="button ~neutral @low settings-search-clear">
<span class="mr-2">{{ .strings.clearSearch }}</span><i class="ri-close-line"></i>
<div class="flex flex-col gap-4 h-[100%] justify-center items-center">
<span class="text-2xl font-medium italic">{{ .strings.noResultsFound }}</span>
<span class="px-12 text-center">{{ .strings.settingsMaybeUnderAdvanced }}</span>
<button class="button ~neutral @low settings-search-clear flex flex-row gap-2">
<span>{{ .strings.clearSearch }}</span><i class="ri-close-line"></i>
</button>
</div>
</div>
@@ -931,5 +923,5 @@
</div>
</div>
<script src="{{ .pages.Base }}/js/admin.js" type="module"></script>
</body>
</div></body>
</html>

View File

@@ -1,42 +1,38 @@
<!doctype html>
<html lang="en">
<html lang="{{ .shortLang }}" dir="{{ .pageDirection }}">
<head>
<!--- This CSS is inlined so we should keep this here! -->
<link inline rel="stylesheet" type="text/css" href="web/css/v0.6.0bundle.css">
{{ template "header.html" . }}
{{ template "header.txt" . }}
<title>Crash report</title>
</head>
<body>
<div class="page-container m-2 lg:my-20 lg:mx-64">
<div class="card ~critical sectioned">
<section class="section ~critical">
<section class="section ~critical flex flex-col gap-2">
<span class="heading">Crash report for jfa-go</span>
{{ if .Err }}
<div class="font-mono bg-inherit pre-line mt-4 mb-4">
<div class="font-mono bg-inherit pre-line">
Error: {{ .Err }}
</div>
{{ end }}
<a class="button ~critical mb-4" target="_blank" href="https://github.com/hrfee/jfa-go/issues/new/choose">Create an Issue</a>
<a class="button ~critical w-full center" target="_blank" href="https://github.com/hrfee/jfa-go/issues/new/choose">Create an Issue</a>
</section>
<section class="section ~neutral @low">
<div class="flex flex-row justify-between">
<span class="subheading">Full Log</span>
<span class="button ~urge ml-4" id="copy-log">Copy</span>
<section class="section ~neutral @low flex flex-col gap-4">
<div class="flex flex-row justify-between gap-4">
<span class="subheading font-medium">Full Log</span>
<span class="button ~urge" id="copy-log">Copy</span>
</div>
<div class="row mb-4">
<label class="col mr-4">
<span class="button ~neutral @high supra full-width center" id="button-log-normal">Normal</span>
</label>
<label class="col mr-4">
<span class="button ~neutral @low supra full-width center" id="button-log-sanitized">Sanitized</span>
</label>
<div class="flex flex-row gap-2 justify-between">
<button class="button ~neutral @high supra w-full center" id="button-log-normal">Normal</button>
<button class="button ~neutral @low supra w-full center" id="button-log-sanitized">Sanitized</button>
</div>
<div id="log-normal">
<pre class="font-mono bg-inherit pre-line">{{ .Log }}</pre>
<pre class="card font-mono bg-inherit pre-line">{{ .Log }}</pre>
</div>
<div id="log-sanitized" class="unfocused">
<p class="subheading">An attempt has been made to remove sensitive info, but make sure to check yourself.</p>
<pre class="font-mono bg-inherit pre-line">{{ .SanitizedLog }}</pre>
<div id="log-sanitized" class="flex flex-col gap-2 unfocused">
<p class="support subheading">An attempt has been made to remove sensitive info, but make sure to check yourself.</p>
<pre class="card font-mono bg-inherit pre-line">{{ .SanitizedLog }}</pre>
</div>
</section>
</div>

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<html lang="{{ .shortLang }}" dir="{{ .pageDirection }}" class="{{ .cssClass }}">
<head>
{{ template "header.html" . }}
{{ template "header.txt" . }}
<title>{{ .strings.successHeader }} - jfa-go</title>
</head>
<body class="section">

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<html lang="{{ .shortLang }}" dir="{{ .pageDirection }}" class="{{ .cssClass }}">
<head>
{{ template "header.html" . }}
{{ template "header.txt" . }}
{{ if .passwordReset }}
<title>{{ .strings.passwordReset }}</title>
{{ else }}
@@ -41,7 +41,7 @@
</div>
<div class="card dark:~d_neutral @low">
<div class="flex flex-col md:flex-row gap-3 items-baseline mb-2">
<span class="heading mr-5">
<span class="heading">
{{ if .passwordReset }}
{{ .strings.passwordReset }}
{{ else }}
@@ -82,23 +82,23 @@
<span class="button ~info @low full-width center mb-4" id="link-matrix">{{ .strings.linkMatrix }} {{ if .matrixRequired }}({{ .strings.required }}){{ end }}</span>
{{ end }}
{{ if or (.telegramEnabled) (or .discordEnabled .matrixEnabled) }}
<div id="contact-via" class="unfocused">
<label class="row switch pb-4 unfocused">
<input type="checkbox" name="contact-via" value="email" id="contact-via-email" class="mr-2"><span>Contact through Email</span>
<div id="contact-via" class="unfocused flex flex-col gap-2">
<label class="flex flex-row gap-2 switch unfocused">
<input type="checkbox" name="contact-via" value="email" id="contact-via-email"><span>Contact through Email</span>
</label>
{{ if .telegramEnabled }}
<label class="row switch pb-4 unfocused">
<input type="checkbox" name="contact-via" value="telegram" id="contact-via-telegram" class="mr-2"><span>Contact through Telegram</span>
<label class="flex flex-row gap-2 switch unfocused">
<input type="checkbox" name="contact-via" value="telegram" id="contact-via-telegram"><span>Contact through Telegram</span>
</label>
{{ end }}
{{ if .discordEnabled }}
<label class="row switch pb-4 unfocused">
<input type="checkbox" name="contact-via" value="discord" id="contact-via-discord" class="mr-2"><span>Contact through Discord</span>
<label class="flex flex-row gap-2 switch unfocused">
<input type="checkbox" name="contact-via" value="discord" id="contact-via-discord"><span>Contact through Discord</span>
</label>
{{ end }}
{{ if .matrixEnabled }}
<label class="row switch pb-4 unfocused">
<input type="checkbox" name="contact-via" value="matrix" id="contact-via-matrix" class="mr-2"><span>Contact through Matrix</span>
<label class="flex flex-row gap-2 switch unfocused">
<input type="checkbox" name="contact-via" value="matrix" id="contact-via-matrix"><span>Contact through Matrix</span>
</label>
{{ end }}
</div>
@@ -125,6 +125,11 @@
{{ if .fromUser }}
<aside class="col aside sm ~positive mb-4" id="invite-from-user" data-from="{{ .fromUser }}">{{ .strings.invitedBy }}</aside>
{{ end }}
{{ if .preSignupCard }}
<div class="card @low dark:~d_neutral break-words content">
{{ .preSignupCardContent }}
</div>
{{ end }}
<div class="card ~neutral @low mb-4">
<span class="label supra">{{ .strings.passwordRequirementsHeader }}</span>
<ul>
@@ -136,11 +141,19 @@
</ul>
</div>
{{ if .captcha }}
<div class="card ~neutral @low mb-4">
<span class="label supra mb-2">CAPTCHA {{ if not .reCAPTCHA }}<span id="captcha-regen" title="{{ .strings.refresh }}" class="badge lg @low ~info ml-2 float-right"><i class="ri-refresh-line"></i></span><span id="captcha-success" class="badge lg @low ~critical ml-2 float-right"><i class="ri-close-line"></i></span>{{ end }}</span>
<div id="captcha-img" class="mt-2 mb-2 {{ if .reCAPTCHA }}g-recaptcha{{ end }}"></div>
<div class="card ~neutral @low mb-4 flex flex-col gap-2">
<div class="flex flex-row justify-between gap-2">
<span class="label supra">CAPTCHA</span>
{{ if not .reCAPTCHA }}
<div class="flex flex-row gap-2">
<button id="captcha-regen" aria-label="{{ .strings.refresh }}" title="{{ .strings.refresh }}" class="badge lg @low ~info"><i class="ri-refresh-line"></i></button>
<span id="captcha-success" class="badge lg @low ~critical"><i class="ri-close-line"></i></span>
</div>
{{ end }}
</div>
<div id="captcha-img" class="{{ if .reCAPTCHA }}g-recaptcha{{ end }}"></div>
{{ if not .reCAPTCHA }}
<input class="field ~neutral @low" id="captcha-input" class="mt-2" placeholder="CAPTCHA">
<input class="field ~neutral @low" id="captcha-input" placeholder="CAPTCHA">
{{ end }}
</div>
{{ end }}

View File

@@ -3,6 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="Description" content="jfa-go, a better way to manage Jellyfin users.">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#101010" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff">
<meta name="robots" content="noindex">
<link rel="apple-touch-icon" sizes="180x180" href="{{ .pages.Base }}/apple-touch-icon.png">
@@ -11,7 +14,6 @@
<link rel="manifest" href="{{ .pages.Base }}/site.webmanifest">
<link rel="mask-icon" href="{{ .pages.Base }}/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#603cba">
<meta name="theme-color" content="#ffffff">
<script>
window.pages = {
"Base": "{{ .pages.Base }}",

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<html lang="{{ .shortLang }}" dir="{{ .pageDirection }}" class="{{ .cssClass }}">
<head>
{{ template "header.html" . }}
{{ template "header.txt" . }}
<title>Invalid Code - jfa-go</title>
</head>
<body class="section">

View File

@@ -1,7 +1,7 @@
<span class="dropdown z-[11]" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
<i class="ri-global-line"></i>
<span class="ml-2 chev"></span>
<span class="button ~urge dropdown-button flex flex-row gap-2 h-full" title="{{ .strings.language }}" aria-label="{{ .strings.language }}">
<i class="icon ri-global-line"></i>
<i class="icon ri-arrow-down-s-line"></i>
</span>
<div class="dropdown-display">
<div class="card ~neutral @low flex flex-col gap-2">
@@ -13,7 +13,7 @@
<input type="radio" name="lang-time" id="lang-24h">
<span>{{ .strings.time24h }}</span>
</label>
<div id="lang-list"></div>
<div id="lang-list" class="flex flex-col gap-2"></div>
</div>
</div>
</span>

View File

@@ -15,23 +15,23 @@
{{ $hasTwoCards = 1 }}
<div class="card mx-2 flex-initial w-full lg:w-[35%] mb-4 lg:mb-0 dark:~d_neutral @low content">
<span class="heading row">{{ .strings.loginNotAdmin }}</span>
<a class="button ~info h-12 w-full" href="{{ .pages.Base }}{{ .pages.MyAccount }}"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
<a class="button ~info h-12 w-full flex flex-row gap-2" href="{{ .pages.Base }}{{ .pages.MyAccount }}"><i class="ri-account-circle-fill"></i>{{ .strings.myAccount }}</a>
</div>
{{ end }}
{{ end }}
<form class="card mx-2 form-login w-full {{ if eq $hasTwoCards 1 }}lg:w-[55%]{{ end }} mb-0" href="">
<form class="card mx-2 form-login w-full flex flex-col gap-2 {{ if eq $hasTwoCards 1 }}lg:w-[55%]{{ end }} mb-0" href="">
<span class="heading">{{ .strings.login }}</span>
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="login-user">
<input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="login-password">
<input type="text" class="field input ~neutral @high" placeholder="{{ .strings.username }}" id="login-user">
<input type="password" class="field input ~neutral @high" placeholder="{{ .strings.password }}" id="login-password">
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.login }}</span>
{{ if index . "pwrEnabled" }}
{{ if .pwrEnabled }}
<span class="button ~info @low full-width center supra submit my-2" id="modal-login-pwr">{{ .strings.resetPassword }}</span>
{{ end }}
{{ end }}
</label>
{{ if index . "pwrEnabled" }}
{{ if .pwrEnabled }}
<span class="button ~info @low full-width center supra" id="modal-login-pwr">{{ .strings.resetPassword }}</span>
{{ end }}
{{ end }}
</form>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<html lang="{{ .shortLang }}" dir="{{ .pageDirection }}" class="{{ .cssClass }}">
<head>
{{ template "header.html" . }}
{{ template "header.txt" . }}
<title>{{ .strings.passwordReset }} - jfa-go</title>
</head>
<body class="section">

View File

@@ -1,12 +1,12 @@
<!DOCTYPE html>
<html lang="en" class="light">
<html lang="{{ .shortLang }}" dir="{{ .pageDirection }}" class="light">
<head>
{{ template "header.html" . }}
{{ template "header.txt" . }}
<title>{{ .lang.Strings.pageTitle }}</title>
</head>
<body class="max-w-full overflow-x-hidden section">
<div id="notification-box"></div>
<div class="page-container m-2 lg:my-20 lg:mx-64 flex flex-col gap-4 items-center">
<div class="page-container m-2 lg:my-20 lg:mx-64 flex flex-col gap-4">
<div class="top-2 inset-x-2 lg:absolute flex flex-row justify-between">
<div class="flex flex-row gap-2">
{{ template "lang-select.html" . }}
@@ -322,10 +322,10 @@
</label>
<div class="flex flex-col gap-2">
<label class="label flex flex-col gap-2">
<div class="switch"><input type="radio" class="mr-2" name="email-24h" value="true" checked><span>{{ .lang.Strings.time24h }}</span></div>
<div class="switch flex flex-row gap-2"><input type="radio" name="email-24h" value="true" checked><span>{{ .lang.Strings.time24h }}</span></div>
</label>
<label class="label flex flex-col gap-2">
<div class="switch"><input type="radio" class="mr-2" name="email-24h" value="false"><span>{{ .lang.Strings.time12h }}</span></div>
<div class="switch flex flex-row gap-2"><input type="radio" name="email-24h" value="false"><span>{{ .lang.Strings.time12h }}</span></div>
</label>
</div>
<div id="email-sect" class="flex flex-row gap-2 justify-between">

View File

@@ -0,0 +1,3 @@
<link rel="stylesheet" type="text/css" href="{{ .pages.Base }}/css/{{ .cssVersion }}highlightjs-light.css" data-theme="light">
<link rel="stylesheet" type="text/css" href="{{ .pages.Base }}/css/{{ .cssVersion }}highlightjs-dark.css" data-theme="dark">
<link rel="stylesheet" type="text/css" href="{{ .pages.Base }}/css/{{ .cssVersion }}code-input.css">

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="light">
<html lang="{{ .shortLang }}" dir="{{ .pageDirection }}" class="light">
<head>
<script>
window.langFile = JSON.parse({{ .language }});
@@ -17,52 +17,50 @@
window.matrixUserID = "{{ .matrixUser }}";
window.validationStrings = JSON.parse({{ .validationStrings }});
</script>
{{ template "header.html" . }}
{{ template "header.txt" . }}
<title>{{ .strings.myAccount }}</title>
</head>
<body class="max-w-full overflow-x-hidden section">
<div id="modal-email" class="modal">
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
<div class="content">
<span class="heading mb-4 my-2"></span>
<label class="label supra row m-1" for="modal-email-input">{{ .strings.emailAddress }}</label>
<div class="row">
<input type="email" class="col sm field ~neutral @low input" id="modal-email-input" placeholder="{{ .strings.emailAddress }}">
</div>
<button class="button ~urge @low supra full-width center lg my-2 modal-submit">{{ .strings.submit }}</button>
<div class="flex flex-col gap-2">
<span class="heading"></span>
<label class="label flex flex-col gap-2">
<span class="supra">{{ .strings.emailAddress }}</span>
<input type="email" class="field ~neutral @low input" id="modal-email-input" placeholder="{{ .strings.emailAddress }}">
</label>
<button class="button ~urge @low supra full-width center lg modal-submit">{{ .strings.submit }}</button>
</div>
<div class="confirmation-required unfocused">
<span class="heading mb-4">{{ .strings.confirmationRequired }} <span class="modal-close">&times;</span></span>
<p class="content mb-4">{{ .strings.confirmationRequiredMessage }}</p>
<div class="confirmation-required unfocused flex flex-col gap-2">
<span class="heading">{{ .strings.confirmationRequired }} <span class="modal-close">&times;</span></span>
<p class="content">{{ .strings.confirmationRequiredMessage }}</p>
</div>
</div>
</div>
{{ if .pwrEnabled }}
<div id="modal-pwr" class="modal">
<div class="card content relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low">
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low flex flex-col gap-2">
<span class="heading">{{ .strings.resetPassword }}</span>
<p class="content my-2">
{{ if .linkResetEnabled }}
{{ .strings.resetPasswordThroughLinkStart }}
<ul class="content">
{{ if .resetPasswordUsername }}<li>{{ .strings.resetPasswordUsername }}</li>{{ end }}
{{ if .resetPasswordEmail }}<li>{{ .strings.resetPasswordEmail }}</li>{{ end }}
{{ if .resetPasswordContactMethod }}<li>{{ .strings.resetPasswordContactMethod }}</li>{{ end }}
</ul>
{{ .strings.resetPasswordThroughLinkEnd }}
{{ else }}
{{ .strings.resetPasswordThroughJellyfin }}
{{ end }}
</p>
<div class="row">
<input type="text" class="col sm field ~neutral @low input" id="pwr-address" placeholder="username | example@example.com | user#1234 | @user:host | @username">
<div class="content">
{{ if .linkResetEnabled }}
<p>{{ .strings.resetPasswordThroughLinkStart }}</p>
<ul class="content">
{{ if .resetPasswordUsername }}<li>{{ .strings.resetPasswordUsername }}</li>{{ end }}
{{ if .resetPasswordEmail }}<li>{{ .strings.resetPasswordEmail }}</li>{{ end }}
{{ if .resetPasswordContactMethod }}<li>{{ .strings.resetPasswordContactMethod }}</li>{{ end }}
</ul>
<p>{{ .strings.resetPasswordThroughLinkEnd }}</p>
{{ else }}
<p>{{ .strings.resetPasswordThroughJellyfin }}</p>
{{ end }}
</div>
<input type="text" class="col sm field ~neutral @low input" id="pwr-address" placeholder="username | example@example.com | user#1234 | @user:host | @username">
{{ if .linkResetEnabled }}
<span class="button ~info @low full-width center mt-4" id="pwr-submit">
<span class="button ~info @low full-width center" id="pwr-submit">
{{ .strings.submit }}
</span>
{{ else }}
<a class="button ~info @low full-width center mt-4" href="{{ .jfLink }}" target="_blank">{{ .strings.continue }}</a>
<a class="button ~info @low full-width center" href="{{ .jfLink }}" target="_blank">{{ .strings.continue }}</a>
{{ end }}
</div>
</div>
@@ -75,12 +73,12 @@
<div class="flex flex-row gap-2">
{{ template "lang-select.html" . }}
<span class="button ~warning h-min" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
<span class="button ~critical @low unfocused" id="logout-button">{{ .strings.logout }}</span>
</div>
<a class="button ~info unfocused h-min" href="/" id="admin-back-button"><i class="ri-arrow-left-fill mr-2"></i>{{ .strings.admin }}</a>
<a class="button ~info unfocused h-min flex flex-row gap-1" href="/" id="admin-back-button"><i class="ri-arrow-left-fill"></i>{{ .strings.admin }}</a>
</div>
<div class="card @low dark:~d_neutral mb-4" id="card-user">
<span class="heading mb-2"></span>
<div class="card @low dark:~d_neutral" id="card-user">
<span class="heading flex flex-row gap-4"></span>
</div>
<div class="columns-1 sm:columns-2 gap-4" id="user-cardlist">
{{ if index . "PageMessageEnabled" }}
@@ -90,15 +88,15 @@
</div>
{{ end }}
{{ end }}
<div class="card @low dark:~d_neutral flex-col" id="card-contact">
<span class="heading mb-2">{{ .strings.contactMethods }}</span>
<div class="card @low dark:~d_neutral flex flex-col gap-2" id="card-contact">
<span class="heading">{{ .strings.contactMethods }}</span>
<div class="content flex justify-between flex-col h-100"></div>
</div>
<div>
<div class="card @low dark:~d_neutral content" id="card-password">
<span class="heading row mb-2">{{ .strings.changePassword }}</span>
<div class="">
<div class="my-2">
<div class="card @low dark:~d_neutral flex flex-col gap-2" id="card-password">
<span class="heading">{{ .strings.changePassword }}</span>
<div class="flex flex-col gap-2">
<div class="content">
<span class="label supra row">{{ .strings.passwordRequirementsHeader }}</span>
<ul>
{{ range $key, $value := .requirements }}
@@ -108,15 +106,15 @@
{{ end }}
</ul>
</div>
<div class="my-2">
<div class="flex flex-col gap-2">
<label class="label supra" for="user-old-password">{{ .strings.oldPassword }}</label>
<input type="password" class="input ~neutral @low mt-2 mb-4" placeholder="{{ .strings.password }}" id="user-old-password" aria-label="{{ .strings.oldPassword }}">
<input type="password" class="input ~neutral @low" placeholder="{{ .strings.password }}" id="user-old-password" aria-label="{{ .strings.oldPassword }}">
<label class="label supra" for="user-new-password">{{ .strings.newPassword }}</label>
<input type="password" class="input ~neutral @low mt-2 mb-4" placeholder="{{ .strings.password }}" id="user-new-password" aria-label="{{ .strings.newPassword }}">
<input type="password" class="input ~neutral @low" placeholder="{{ .strings.password }}" id="user-new-password" aria-label="{{ .strings.newPassword }}">
<label class="label supra" for="user-reenter-password">{{ .strings.reEnterPassword }}</label>
<input type="password" class="input ~neutral @low mt-2 mb-4" placeholder="{{ .strings.password }}" id="user-reenter-new-password" aria-label="{{ .strings.reEnterPassword }}">
<span class="button ~info @low full-width center mt-4" id="user-password-submit">
<input type="password" class="input ~neutral @low" placeholder="{{ .strings.password }}" id="user-reenter-new-password" aria-label="{{ .strings.reEnterPassword }}">
<span class="button ~info @low full-width center" id="user-password-submit">
{{ .strings.changePassword }}
</span>
</div>
@@ -124,21 +122,21 @@
</div>
</div>
<div>
<div class="card @low dark:~d_neutral unfocused" id="card-status">
<span class="heading mb-2">{{ .strings.expiry }}</span>
<aside class="aside ~warning user-expiry my-4"></aside>
<div class="card @low dark:~d_neutral unfocused flex flex-col gap-2" id="card-status">
<span class="heading">{{ .strings.expiry }}</span>
<aside class="aside ~warning user-expiry"></aside>
<div class="user-expiry-countdown"></div>
</div>
</div>
{{ if .referralsEnabled }}
<div>
<div class="card @low dark:~d_neutral unfocused" id="card-referrals">
<span class="heading mb-2">{{ .strings.referrals }}</span>
<aside class="aside ~neutral my-4 col user-referrals-description"></aside>
<div class="card @low dark:~d_neutral unfocused flex flex-col gap-2" id="card-referrals">
<span class="heading">{{ .strings.referrals }}</span>
<aside class="aside ~neutral col user-referrals-description"></aside>
<div class="flex flex-row justify-between gap-2">
<div class="user-referrals-info"></div>
<div class="grid my-2">
<button type="button" class="user-referrals-button button ~info dark:~d_info @low" title="Copy">{{ .strings.copyReferral }}<i class="ri-file-copy-line ml-2"></i></button>
<div class="user-referrals-info flex flex-col gap-2"></div>
<div class="grid">
<button type="button" class="user-referrals-button button ~info dark:~d_info @low flex flex-row gap-2" title="Copy">{{ .strings.copyReferral }}<i class="ri-file-copy-line"></i></button>
</div>
</div>
</div>

View File

@@ -14,7 +14,7 @@ const binaryType = "internal"
func BuildTagsExternal() {}
//go:embed data data/html data/web data/web/css data/web/js
//go:embed build/data build/data/html build/data/web build/data/web/css build/data/web/js
var loFS embed.FS
//go:embed lang/common lang/admin lang/email lang/form lang/setup lang/pwreset lang/telegram
@@ -38,6 +38,6 @@ func FSJoin(elem ...string) string {
func loadFilesystems(rootDir string, logger *logger.Logger) {
langFS = rewriteFS{laFS, "lang/"}
localFS = rewriteFS{loFS, "data/"}
localFS = rewriteFS{loFS, "build/data/"}
logger.Println("Using internal storage")
}

View File

@@ -9,8 +9,13 @@ import (
lm "github.com/hrfee/jfa-go/logmessages"
)
type JellyseerrInitialSyncStatus struct {
Done bool
}
// Ensure the Jellyseerr cache is up to date before calling.
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
user, imported, err := app.js.GetOrImportUser(jfID)
user, imported, err := app.js.GetOrImportUser(jfID, true)
if err != nil {
app.debug.Printf(lm.FailedImportUser, lm.Jellyseerr, jfID, err)
return
@@ -32,7 +37,6 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
if strings.Contains(err.Error(), "INVALID_EMAIL") {
app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, jfID, err.Error()+"\""+email.Addr+"\"")
} else {
app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, jfID, err)
}
} else {
@@ -51,7 +55,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
chatID, _ := strconv.ParseInt(notif.TelegramChatID, 10, 64)
if ok && tgUser.ChatID != 0 && chatID != tgUser.ChatID {
u, _ := app.storage.GetTelegramKey(jfID)
contactMethods[jellyseerr.FieldTelegram] = u.ChatID
contactMethods[jellyseerr.FieldTelegram] = strconv.FormatInt(u.ChatID, 10)
contactMethods[jellyseerr.FieldTelegramEnabled] = tgUser.Contact
}
}
@@ -64,19 +68,30 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
}
func (app *appContext) SynchronizeJellyseerrUsers() {
jsSync := JellyseerrInitialSyncStatus{}
app.storage.db.Get("jellyseerr_inital_sync_status", &jsSync)
if jsSync.Done {
return
}
users, err := app.jf.GetUsers(false)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
return
}
app.js.ReloadCache()
// I'm sure Jellyseerr can handle it,
// but past issues with the Jellyfin db scare me from
// running these concurrently. W/e, its a bg task anyway.
for _, user := range users {
app.SynchronizeJellyseerrUser(user.ID)
}
// Don't run again until this flag is unset
// Stored in the DB as it's not something the user needs to see.
app.storage.db.Upsert("jellyseerr_inital_sync_status", JellyseerrInitialSyncStatus{true})
}
// Not really a normal daemon, since it'll only fire once when the feature is enabled.
func newJellyseerrDaemon(interval time.Duration, app *appContext) *GenericDaemon {
d := NewGenericDaemon(interval, app,
func(app *appContext) {
@@ -84,5 +99,12 @@ func newJellyseerrDaemon(interval time.Duration, app *appContext) *GenericDaemon
},
)
d.Name("Jellyseerr import")
jsSync := JellyseerrInitialSyncStatus{}
app.storage.db.Get("jellyseerr_inital_sync_status", &jsSync)
if jsSync.Done {
return nil
}
return d
}

View File

@@ -25,7 +25,9 @@ type Jellyseerr struct {
server, key string
header map[string]string
httpClient *http.Client
userCache map[string]User // Map of jellyfin IDs to users
userCache map[string]User // Map of jellyfin IDs to users
jsToJfID map[int64]string // Map of jellyseerr IDs to jellyfin IDs
invalidatedUsers map[int64]bool // Map of jellyseerr IDs needing a re-caching
cacheExpiry time.Time
cacheLength time.Duration
timeoutHandler co.TimeoutHandler
@@ -51,6 +53,8 @@ func NewJellyseerr(server, key string, timeoutHandler co.TimeoutHandler) *Jellys
cacheExpiry: time.Now(),
timeoutHandler: timeoutHandler,
userCache: map[string]User{},
jsToJfID: map[int64]string{},
invalidatedUsers: map[int64]bool{},
LogRequestBodies: false,
}
}
@@ -158,6 +162,7 @@ func (js *Jellyseerr) ImportFromJellyfin(jfIDs ...string) ([]User, error) {
for _, u := range data {
if u.JellyfinUserID != "" {
js.userCache[u.JellyfinUserID] = u
js.jsToJfID[u.ID] = u.JellyfinUserID
}
}
return data, err
@@ -166,8 +171,13 @@ func (js *Jellyseerr) ImportFromJellyfin(jfIDs ...string) ([]User, error) {
func (js *Jellyseerr) getUsers() error {
if js.cacheExpiry.After(time.Now()) {
return nil
if len(js.invalidatedUsers) != 0 {
return js.getInvalidatedUsers()
}
}
js.cacheExpiry = time.Now().Add(js.cacheLength)
userCache := map[string]User{}
jsToJfID := map[int64]string{}
pageCount := 1
pageIndex := 0
for {
@@ -179,7 +189,8 @@ func (js *Jellyseerr) getUsers() error {
if u.JellyfinUserID == "" {
continue
}
js.userCache[u.JellyfinUserID] = u
userCache[u.JellyfinUserID] = u
jsToJfID[u.ID] = u.JellyfinUserID
}
pageCount = res.Page.Pages
pageIndex++
@@ -187,6 +198,10 @@ func (js *Jellyseerr) getUsers() error {
break
}
}
js.userCache = userCache
js.jsToJfID = jsToJfID
js.invalidatedUsers = map[int64]bool{}
return nil
}
@@ -207,15 +222,15 @@ func (js *Jellyseerr) getUserPage(page int) (GetUsersDTO, error) {
}
func (js *Jellyseerr) MustGetUser(jfID string) (User, error) {
u, _, err := js.GetOrImportUser(jfID)
u, _, err := js.GetOrImportUser(jfID, false)
return u, err
}
// GetImportedUser provides the same function as ImportFromJellyfin, but will always return the user,
// even if they already existed. Also returns whether the user was imported or not,
func (js *Jellyseerr) GetOrImportUser(jfID string) (u User, imported bool, err error) {
func (js *Jellyseerr) GetOrImportUser(jfID string, fixedCache bool) (u User, imported bool, err error) {
imported = false
u, err = js.GetExistingUser(jfID)
u, err = js.GetExistingUser(jfID, fixedCache)
if err == nil {
return
}
@@ -233,15 +248,24 @@ func (js *Jellyseerr) GetOrImportUser(jfID string) (u User, imported bool, err e
return
}
func (js *Jellyseerr) GetExistingUser(jfID string) (u User, err error) {
func (js *Jellyseerr) GetExistingUser(jfID string, fixedCache bool) (u User, err error) {
js.getUsers()
ok := false
err = nil
if u, ok = js.userCache[jfID]; ok {
u, ok = js.userCache[jfID]
_, invalidated := js.invalidatedUsers[u.ID]
if ok && !invalidated {
return
}
js.cacheExpiry = time.Now()
js.getUsers()
if invalidated {
err = js.getInvalidatedUsers()
if err != nil {
return
}
} else if !fixedCache {
js.cacheExpiry = time.Now()
js.getUsers()
}
if u, ok = js.userCache[jfID]; ok {
err = nil
return
@@ -254,7 +278,7 @@ func (js *Jellyseerr) getUser(jfID string) (User, error) {
if js.AutoImportUsers {
return js.MustGetUser(jfID)
}
return js.GetExistingUser(jfID)
return js.GetExistingUser(jfID, false)
}
func (js *Jellyseerr) Me() (User, error) {
@@ -268,6 +292,25 @@ func (js *Jellyseerr) Me() (User, error) {
return data, err
}
func (js *Jellyseerr) getInvalidatedUsers() error {
// FIXME: Collect errors and return
for jellyseerrID, _ := range js.invalidatedUsers {
jfID, ok := js.jsToJfID[jellyseerrID]
if !ok {
continue
}
user, err := js.UserByID(jellyseerrID)
if err != nil {
continue
}
js.userCache[jfID] = user
js.jsToJfID[jellyseerrID] = jfID
delete(js.invalidatedUsers, jellyseerrID)
}
return nil
}
func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
data := permissionsDTO{Permissions: -1}
u, err := js.getUser(jfID)
@@ -295,6 +338,7 @@ func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
}
u.Permissions = perm
js.userCache[jfID] = u
js.jsToJfID[u.ID] = jfID
return nil
}
@@ -310,6 +354,7 @@ func (js *Jellyseerr) ApplyTemplateToUser(jfID string, tmpl UserTemplate) error
}
u.UserTemplate = tmpl
js.userCache[jfID] = u
js.jsToJfID[u.ID] = jfID
return nil
}
@@ -326,8 +371,7 @@ func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]any) error {
if err != nil {
return err
}
// Lazily just invalidate the cache.
js.cacheExpiry = time.Now()
js.invalidatedUsers[u.ID] = true
return nil
}
@@ -413,12 +457,19 @@ func (js *Jellyseerr) ModifyMainUserSettings(jfID string, conf MainUserSettings)
if err != nil {
return err
}
return js.ModifyMainUserSettingsByID(u.ID, conf)
}
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
func (js *Jellyseerr) ModifyMainUserSettingsByID(jellyseerrID int64, conf MainUserSettings) error {
_, _, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/main", jellyseerrID), conf, false)
if err != nil {
return err
}
// Lazily just invalidate the cache.
js.cacheExpiry = time.Now()
js.invalidatedUsers[jellyseerrID] = true
return nil
}
func (js *Jellyseerr) ReloadCache() error {
js.cacheExpiry = time.Now()
return js.getUsers()
}

248
jf_activity.go Normal file
View File

@@ -0,0 +1,248 @@
package main
import (
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/hrfee/mediabrowser"
)
const (
// ActivityLimit is the maximum number of ActivityLogEntries to keep in memory.
// The array they are stored in is fixed, so (ActivityLimit*unsafe.Sizeof(mediabrowser.ActivityLogEntry))
// At writing ActivityLogEntries take up ~160 bytes each, so 1M of memory gives us room for ~6250 records
ActivityLimit int = 1e6 / 160
// If ByUserLimitLength is true, ByUserLengthOrBaseLength is the maximum number of records attached
// to a user.
// If false, it is the base amount of entries to allocate for for each user ID, and more will be allocated as needed.
ByUserLengthOrBaseLength = 128
ByUserLimitLength = false
)
type activityLogEntrySource interface {
GetActivityLog(skip, limit int, since time.Time, hasUserID bool) (mediabrowser.ActivityLog, error)
}
// JFActivityCache is a cache for Jellyfin ActivityLogEntries, intended to be refreshed frequently
// and suited to it by only querying for changes since the last refresh.
type JFActivityCache struct {
jf activityLogEntrySource
cache [ActivityLimit]mediabrowser.ActivityLogEntry
// index into Cache of the entry that should be considered the start (i.e. most recent), and end (i.e. oldest).
start, end int
// Map of activity entry IDs to their index.
byEntryID map[int64]int
// Map of user IDs to a slice of entry indexes they are referenced in, chronologically ordered.
byUserID map[string][]int
LastSync, LastYieldingSync time.Time
// Age of cache before it should be refreshed.
WaitForSyncTimeout time.Duration
syncLock sync.Mutex
syncing bool
// Total number of entries.
Total int
dupesInLastSync int
}
func (c *JFActivityCache) debugString() string {
var b strings.Builder
places := len(strconv.Itoa(ActivityLimit - 1))
b.Grow((ActivityLimit * (places + 1) * 2) + 1)
for i := range c.cache {
fmt.Fprintf(&b, "%0"+strconv.Itoa(places)+"d|", i)
}
b.WriteByte('\n')
for i := range c.cache {
fmt.Fprintf(&b, "%0"+strconv.Itoa(places)+"d|", c.cache[i].ID)
}
return b.String()
}
// NewJFActivityCache returns a Jellyfin ActivityLogEntry cache.
// You should set the timeout low, as events are likely to happen frequently,
// and refreshing should be quick anyway
func NewJFActivityCache(jf activityLogEntrySource, waitForSyncTimeout time.Duration) *JFActivityCache {
c := &JFActivityCache{
jf: jf,
WaitForSyncTimeout: waitForSyncTimeout,
start: -1,
end: -1,
byEntryID: map[int64]int{},
byUserID: map[string][]int{},
Total: 0,
dupesInLastSync: 0,
}
for i := range ActivityLimit {
c.cache[i].ID = -1
}
return c
}
// ByUserID returns a slice of ActivitLogEntries with the given jellyfin ID attached.
func (c *JFActivityCache) ByUserID(jellyfinID string) ([]mediabrowser.ActivityLogEntry, error) {
if err := c.MaybeSync(); err != nil {
return nil, err
}
arr, ok := c.byUserID[jellyfinID]
if !ok {
return nil, nil
}
out := make([]mediabrowser.ActivityLogEntry, len(arr))
for i, aleIdx := range arr {
out[i] = c.cache[aleIdx]
}
return out, nil
}
// ByEntryID returns the ActivityLogEntry with the corresponding ID.
func (c *JFActivityCache) ByEntryID(entryID int64) (entry mediabrowser.ActivityLogEntry, ok bool, err error) {
err = c.MaybeSync()
if err != nil {
return
}
var idx int
idx, ok = c.byEntryID[entryID]
if !ok {
return
}
entry = c.cache[idx]
return
}
// MaybeSync returns once the cache is in a suitable state to read:
// return if cache is fresh, sync if not, or wait if another sync is happening already.
func (c *JFActivityCache) MaybeSync() error {
shouldWaitForSync := time.Now().After(c.LastSync.Add(c.WaitForSyncTimeout))
if !shouldWaitForSync {
return nil
}
syncStatus := make(chan error)
go func(status chan error, c *JFActivityCache) {
c.syncLock.Lock()
alreadySyncing := c.syncing
// We're either already syncing or will be
c.syncing = true
c.syncLock.Unlock()
if !alreadySyncing {
// If we haven't synced, this'll just get max (ActivityLimit),
// If we have, it'll get anything that's happened since then
thisSync := time.Now()
al, err := c.jf.GetActivityLog(-1, ActivityLimit, c.LastYieldingSync, true)
if err != nil {
c.syncLock.Lock()
c.syncing = false
c.syncLock.Unlock()
status <- err
return
}
// Can't trust the source fully, so we need to check for anything we've already got stored
// -before- we decide where the data should go.
recvLength := len(al.Items)
c.dupesInLastSync = 0
for i, ale := range al.Items {
if _, ok := c.byEntryID[ale.ID]; ok {
c.dupesInLastSync = len(al.Items) - i
// If we got the same as before, everything after it we'll also have.
recvLength = i
break
}
}
if recvLength > 0 {
// Lazy strategy: rebuild user ID maps each time.
// Wipe them, and then append each new refresh element as we process them.
// Then loop through all the old entries and append them too.
for uid := range c.byUserID {
c.byUserID[uid] = c.byUserID[uid][:0]
}
previousStart := c.start
if c.start == -1 {
c.start = 0
c.end = recvLength - 1
} else {
c.start = ((c.start-recvLength)%ActivityLimit + ActivityLimit) % ActivityLimit
}
if c.cache[c.start].ID != -1 {
c.end = ((c.end-1)%ActivityLimit + ActivityLimit) % ActivityLimit
}
for i := range recvLength {
ale := al.Items[i]
ci := (c.start + i) % ActivityLimit
if c.cache[ci].ID != -1 {
// Since we're overwriting it, remove it from index
delete(c.byEntryID, c.cache[ci].ID)
// don't increment total since we're adding and removing
} else {
c.Total++
}
if ale.UserID != "" {
arr, ok := c.byUserID[ale.UserID]
if !ok {
arr = make([]int, 0, ByUserLengthOrBaseLength)
}
if !ByUserLimitLength || len(arr) < ByUserLengthOrBaseLength {
arr = append(arr, ci)
c.byUserID[ale.UserID] = arr
}
}
c.cache[ci] = ale
c.byEntryID[ale.ID] = ci
}
// If this was the first sync, everything has already been processed in the previous loop.
if previousStart != -1 {
i := previousStart
for {
if c.cache[i].UserID != "" {
arr, ok := c.byUserID[c.cache[i].UserID]
if !ok {
arr = make([]int, 0, ByUserLengthOrBaseLength)
}
if !ByUserLimitLength || len(arr) < ByUserLengthOrBaseLength {
arr = append(arr, i)
c.byUserID[c.cache[i].UserID] = arr
}
}
if i == c.end {
break
}
i = (i + 1) % ActivityLimit
}
}
}
// for i := range c.cache {
// fmt.Printf("%04d|", i)
// }
// fmt.Print("\n")
// for i := range c.cache {
// fmt.Printf("%04d|", c.cache[i].ID)
// }
// fmt.Print("\n")
c.syncLock.Lock()
c.LastSync = thisSync
if recvLength > 0 {
c.LastYieldingSync = thisSync
}
c.syncing = false
c.syncLock.Unlock()
} else {
for c.syncing {
continue
}
}
status <- nil
}(syncStatus, c)
err := <-syncStatus
return err
}

136
jf_activity_test.go Normal file
View File

@@ -0,0 +1,136 @@
package main
import (
"sync"
"testing"
"time"
"github.com/hrfee/mediabrowser"
)
type MockActivityLogSource struct {
logs []mediabrowser.ActivityLogEntry
lock sync.Mutex
i int
}
func (m *MockActivityLogSource) run(size int, delay time.Duration, finished *bool) {
m.logs = make([]mediabrowser.ActivityLogEntry, size)
for i := range len(m.logs) {
m.logs[i].ID = -1
}
m.i = 0
for i := range len(m.logs) {
m.lock.Lock()
log := mediabrowser.ActivityLogEntry{
ID: int64(i),
Date: mediabrowser.Time{time.Now()},
}
m.logs[i] = log
m.i = i + 1
m.lock.Unlock()
time.Sleep(delay)
}
*finished = true
time.Sleep(delay)
}
func (m *MockActivityLogSource) GetActivityLog(skip, limit int, since time.Time, hasUserID bool) (mediabrowser.ActivityLog, error) {
// This may introduce duplicates, but those are handled fine.
// If we don't do this, things go wrong in a way that seems
// very specific to this test setup, and (imo) is not necessarily
// applicable to a real scenario.
// since = since.Add(-time.Millisecond)
out := make([]mediabrowser.ActivityLogEntry, 0, limit)
count := 0
loopCount := 0
m.lock.Lock()
for i := m.i - 1; count < limit && i >= 0; i-- {
loopCount++
if m.logs[i].Date.After(since) {
out = append(out, m.logs[i])
count++
}
}
m.lock.Unlock()
return mediabrowser.ActivityLog{Items: out}, nil
}
func TestJFActivityLog(t *testing.T) {
t.Parallel()
// FIXME: This test is failing
t.Run("Completeness", func(t *testing.T) {
mock := MockActivityLogSource{}
waitForSync := time.Microsecond
cache := NewJFActivityCache(&mock, waitForSync)
finished := false
count := len(cache.cache) - 10
go mock.run(count, time.Millisecond, &finished)
for {
if err := cache.MaybeSync(); err != nil {
t.Errorf("sync failed: %v", err)
return
}
if cache.dupesInLastSync > 1 {
t.Logf("got %d dupes in last sync\n", cache.dupesInLastSync)
}
if finished {
// Make sure we got everything
time.Sleep(5 * waitForSync)
if err := cache.MaybeSync(); err != nil {
t.Errorf("sync failed: %v", err)
return
}
break
}
}
t.Log(">-\n" + cache.debugString())
if cache.Total != count {
t.Errorf("not all collected: %d < %d", cache.Total, count)
}
})
t.Run("Ordering", func(t *testing.T) {
mock := MockActivityLogSource{}
waitForSync := 300 * time.Microsecond
cache := NewJFActivityCache(&mock, waitForSync)
finished := false
count := len(cache.cache) * 2
go mock.run(count, time.Millisecond, &finished)
for {
if err := cache.MaybeSync(); err != nil {
t.Errorf("sync failed: %v", err)
return
}
if finished {
// Make sure we got everything
time.Sleep(waitForSync)
if err := cache.MaybeSync(); err != nil {
t.Errorf("sync failed: %v", err)
return
}
break
}
}
t.Log(">-\n" + cache.debugString())
i := cache.start
lastID := int64(-1)
t.Logf("cache start=%d, end=%d, total=%d\n", cache.start, cache.end, cache.Total)
for {
if i != cache.start {
if cache.cache[i].ID != lastID-1 {
t.Errorf("next was not previous ID: %d != %d-1 = %d", cache.cache[i].ID, lastID, lastID-1)
return
}
}
lastID = cache.cache[i].ID
if i == cache.end {
break
}
i = (i + 1) % len(cache.cache)
}
})
}

View File

@@ -11,16 +11,16 @@
"inviteHours": "ساعات",
"inviteMinutes": "دقائق",
"inviteNumberOfUses": "عدد الاستخدامات",
"inviteDuration": "مدة الدعوة",
"inviteDuration": "صلاحية الدعوة",
"warning": "تحذير",
"inviteInfiniteUsesWarning": "الدعوات ذات الاستخدامات اللانهائية يمكن ان تستخدم بشكل مسيئ",
"inviteInfiniteUsesWarning": "الدعوات ذات الاستخدامات اللامحدودة يمكن إساءة استخدامها",
"inviteSendToEmail": "إرسال إلى",
"create": "إنشاء",
"apply": "تطبيق",
"select": "تحديد",
"name": "الاسم",
"date": "التاريخ",
"setExpiry": "تعيين انتهاء الصلاحية",
"setExpiry": "تعيين مدة الصلاحية",
"updates": "التحديثات",
"update": "تحديث",
"download": "تنزيل",
@@ -30,195 +30,316 @@
"from": "من",
"after": "بعد",
"before": "قبل",
"user": "مستخدم",
"userExpiry": "انتهاء صلاحية المستخدم",
"userExpiryDescription": "بعد وقت محدد من تسجيل مستخدم جديد, jfa-go سوف يمسح\\يلغي تفعيل الحساب. بامكانك تغيير هذا السلوك في الاعدادات.",
"aboutProgram": "حول",
"user": "المستخدم",
"userExpiry": "صلاحية المستخدم",
"userExpiryDescription": "عند التفعيل، سيقوم jfa-go بحذف/تعطيل الحساب بعد وقت محدد من التسجيل عبر الدعوة. يمكنك اختيار الإجراء في الإعدادات.",
"aboutProgram": "نُبذة",
"version": "إصدار",
"commitNoun": "فرض",
"commitNoun": "تعديل",
"newUser": "مستخدم جديد",
"profile": "حساب تعريفي",
"profile": "ملف التعريف",
"unknown": "غير معروف",
"label": "وسم",
"label": "الوسم",
"logs": "السجلات",
"announce": "إعلان",
"templates": "قوالب",
"templates": "القوالب",
"subject": "الموضوع",
"message": "الرسالة",
"variables": "المتغيرات",
"conditionals": "",
"conditionals": "الاشتراطات",
"preview": "معاينة",
"reset": "إعادة ضبط",
"donate": "تبرع",
"reset": "إعادة التعيين",
"donate": "تبرّع",
"unlink": "إلغاء ربط الحساب",
"sendPWR": "إرسال إعادة تعيين كلمة المرور",
"contactThrough": "تواصل عن طريق:",
"extendExpiry": "تمديد إنتهاء الصلاحية",
"sendPWRManual": "",
"contactThrough": "تواصل عبر:",
"extendExpiry": "تمديد مدة الصلاحية",
"sendPWRManual": "المستخدم {n} ليس لديه أي وسيلة اتصال، اضغط \"نسخ\" لتحصل على رابط لإرساله إليه.",
"sendPWRSuccess": "تم إرسال رابط إعادة تعيين كلمة المرور.",
"sendPWRSuccessManual": "",
"sendPWRValidFor": "",
"customizeMessages": "",
"customizeMessagesDescription": "",
"markdownSupported": "",
"modifySettings": "",
"modifySettingsDescription": "",
"applyHomescreenLayout": "تطبيق ترتيب الصفحه الرئيسيه",
"sendDeleteNotificationEmail": "ارسال رساله اشعار",
"sendPWRSuccessManual": "إذا لم يستلمه المستخدم، فاضغط \"نسخ\" للحصول على رابط لإرساله إليه يدوياً.",
"sendPWRValidFor": "الرابط صالح لمدة 30 دقيقة.",
"customizeMessages": "تخصيص الرسائل",
"customizeMessagesDescription": "إن لم ترغب في استخدام قوالب رسائل jfa-go، يمكنك إنشاء قوالب مخصصة باستخدام ترميز Markdown.",
"markdownSupported": "ترميز Markdown مدعوم.",
"modifySettings": "تغيير الإعدادات",
"modifySettingsDescription": "طبّق الإعدادات من ملف تعريف موجود، أو انسخها مباشرة من مستخدم.",
"applyHomescreenLayout": "تطبيق مخطط الصفحة الرئيسية",
"sendDeleteNotificationEmail": "إرسال رسالة إشعار",
"sendDeleteNotifiationExample": "تم حذف حسابك.",
"settingsRestart": "اعاده تشغيل",
"settingsRestarting": "اعاده التشغيل…",
"settingsRestartRequired": جب اعاده التشغيل",
"settingsRestartRequiredDescription": جب اعاده التشغيل لتطبيق بعض الاعدادات التي تم تغييرها. اعاده التشغيل الان ام لاحقا؟",
"settingsApplyRestartLater": "تطبيق الاعدادات, اعاده التشغيل لاحقا",
"settingsApplyRestartNow": "تطبيق الاعدادات و اعاده التشغيل",
"settingsApplied": "تم تطبيق الاعدادات.",
"settingsRefreshPage": "اعد انعاش الصفحه بعد بضع ثوان.",
"settingsRequiredOrRestartMessage": "ملاحظه: {n} تشير الى حقل اجباري, {n} تشير ان التغييرات تحتاج لاعاده التشغيل.",
"settingsRestart": "إعادة التشغيل",
"settingsRestarting": "يتم إعادة التشغيل…",
"settingsRestartRequired": لزم إعادة التشغيل",
"settingsRestartRequiredDescription": لزم إعادة التشغيل لتطبيق بعض الإعدادات التي تم تغييرها. هل ترغب بإعادة التشغيل الآن أم لاحقاً؟",
"settingsApplyRestartLater": "تطبيق، إعادة التشغيل لاحقاً",
"settingsApplyRestartNow": "تطبيق وإعادة التشغيل",
"settingsApplied": "تم تطبيق الإعدادات.",
"settingsRefreshPage": "حدّث الصفحة بعد عدة ثوانٍ.",
"settingsRequiredOrRestartMessage": "ملاحظة: {n} تشير إلى حقل مطلوب، و{n} تشير إلى أن التغييرات تتطلب إعادة التشغيل.",
"settingsSave": "حفظ",
"ombiProfile": "",
"ombiUserDefaultsDescription": "",
"userProfiles": "",
"userProfilesDescription": "",
"userProfilesIsDefault": "",
"userProfilesLibraries": "",
"addProfile": "",
"addProfileDescription": "",
"addProfileNameOf": "",
"addProfileStoreHomescreenLayout": "",
"inviteNoUsersCreated": "",
"inviteUsersCreated": "",
"inviteNoProfile": "",
"inviteDateCreated": "",
"ombiProfile": "ملف تعريف مستخدم Ombi",
"ombiUserDefaultsDescription": "أنشئ مستخدم Ombi وقم بإعداده، ثم اختره أدناه. سيتم تخزين إعداداته/أذوناته وتطبيقها على مستخدمي Ombi الجدد الذين أُنشئوا بواسطة jfa-go عند اختيار ملف التعريف هذا.",
"userProfiles": "ملفات التعريف",
"userProfilesDescription": "تُطبّق ملفات التعريف على المستخدمين عند إنشاء حساباتهم. يشمل ملف التعريف صلاحيات الوصول للمكتبات ومخطط الصفحة الرئيسية.",
"userProfilesIsDefault": "الملف الافتراضي",
"userProfilesLibraries": "المكتبات",
"addProfile": "إضافة ملف تعريف",
"addProfileDescription": "أنشئ مستخدم Jellyfin وقم بإعداده، ثم اختره أدناه. عند تطبيق ملف التعريف هذا على دعوة، ستُطبّق إعداداته على المستخدمين المُنشئين من خلال تلك الدعوة.",
"addProfileNameOf": "اسم ملف التعريف",
"addProfileStoreHomescreenLayout": "تخزين مخطط الصفحة الرئيسية",
"inviteNoUsersCreated": "لا أحد حتى الآن!",
"inviteUsersCreated": "المستخدمون المنشئون",
"inviteNoProfile": "بدون ملف تعريف",
"inviteDateCreated": "أُنشئَت في",
"inviteRemainingUses": "",
"inviteNoInvites": "",
"inviteExpiresInTime": "",
"notifyEvent": "",
"notifyInviteExpiry": "",
"notifyUserCreation": "",
"sendPIN": "",
"searchDiscordUser": "",
"findDiscordUser": "",
"linkMatrixDescription": "",
"matrixHomeServer": "",
"saveAsTemplate": "",
"deleteTemplate": "",
"templateEnterName": "",
"accessJFA": "",
"accessJFASettings": "",
"sortingBy": "",
"filters": "",
"clickToRemoveFilter": "",
"clearSearch": "",
"actions": "",
"searchOptions": "",
"matchText": "",
"jellyfinID": "",
"userPageLogin": "",
"userPagePage": "",
"buildTime": "",
"builtBy": "",
"activity": "الانشطه",
"inviteNoInvites": "لا شيء",
"inviteExpiresInTime": "تنتهي بعد {n}",
"notifyEvent": "الإبلاغ عند:",
"notifyInviteExpiry": "عند انتهاء الصلاحية",
"notifyUserCreation": "عند إنشاء مستخدم",
"sendPIN": "اطلب من المستخدم إرسال الرمز أدناه إلى البوت.",
"searchDiscordUser": "أدخل اسم مستخدم Discord للعثور عليه.",
"findDiscordUser": "ابحث عن مستخدم Discord",
"linkMatrixDescription": "أدخل اسم المستخدم وكلمة المرور للمستخدم المراد استخدامه كبوت. بعد إدخالهما، سيُعاد تشغيل التطبيق.",
"matrixHomeServer": "عنوان الخادم الرئيسي",
"saveAsTemplate": "حفظ كقالب",
"deleteTemplate": "حذف القالب",
"templateEnterName": "أدخل اسماً لحفظ هذا القالب.",
"accessJFA": "مسؤول في jfa-go",
"accessJFASettings": "لا يمكن تغيير ذلك حيث تم تفعيل \"المسؤول فقط\" أو \"السماح للجميع\" في الإعدادات > عام.",
"sortingBy": "الفرز حسب",
"filters": "المُرشِّحات",
"clickToRemoveFilter": "اضغط لإزالة المُرشِّح.",
"clearSearch": "إلغاء البحث",
"actions": "الإجراءات",
"searchOptions": "خيارات البحث",
"matchText": "مطابقة النص",
"jellyfinID": "مُعرّف Jellyfin",
"userPageLogin": "صفحة المستخدم: تسجيل الدخول",
"userPagePage": "صفحة المستخدم: الصفحة",
"buildTime": "وقت بناء النُسخة",
"builtBy": "بُنيَت بواسطة",
"activity": "الأنشطة",
"userLabel": "وسم المستخدم",
"userLabelDescription": "الوسام للمستخدمين المفعلين من هذه الدعوه.",
"enableReferrals": "تفعيل الاحالات",
"disableReferrals": "ابطال الاحالات",
"invite": "دعوه",
"enableReferralsProfileDescription": "تمكين المستخدمين من هذا الحساب التعريفي للاحالات الخاصه, لارسالها للعائله\\الاصدقاء. انشاء دعوه بالاعدادات المطلوبه, ثم اختارها هنا. كل احاله سوف تكون مبنيه على اعدادات هذه الدعوه. بامكانك مسح الدعوه عند لانتهاء.",
"enableReferralsDescription": "تمكين المستخدمين لاستعمال احالات خاصه مثل الدعوه, لارسالها للعائله\\للاصدقاء. ممكن اصدارها من قوالب الاحالات في الحساب التعريفي, او من دعوه مفعله."
"userLabelDescription": "وسم يتم تطبيقه على المستخدمين المُنشئين بهذه الدعوة.",
"enableReferrals": "تفعيل الإحالات",
"disableReferrals": "تعطيل الإحالات",
"invite": "دعوة",
"enableReferralsProfileDescription": "امنح المستخدمين المنشئين بملف التعريف هذا رابط إحالة شخصي شبيه بالدعوة، لإرساله إلى الأصدقاء/العائلة. أنشئ دعوة بالإعدادات المطلوبة، ثم اخترها هنا. ستستند كل إحالة بعد ذلك إلى هذه الدعوة. يمكنك حذف الدعوة بمجرد الانتهاء.",
"enableReferralsDescription": "امنح المستخدمين رابط إحالة شخصي شبيه بالدعوة، لإرساله إلى الأصدقاء/العائلة. تستند الإحالة إلى قالب الإحالة في ملف التعريف، أو من دعوة موجودة.",
"disabled": "معطّل",
"wikiPage": "صفحة الويكي",
"wiki": "الويكي",
"enterExpiry": "أدخل تاريخ انتهاء الصلاحية",
"removeExpiry": "إزالة مدة الصلاحية",
"useInviteExpiry": "عيّن مدة الصلاحية من ملف التعريف/الدعوة",
"extendFromPreviousExpiryDescription": "إذا عُثر على تاريخ انتهاء الصلاحية لمستخدم منتهي الصلاحية بالفعل، فسيتم تمديد المدة من ذلك التاريخ، بدلاً من التاريخ الحالي، إلا إذا كان التاريخ الجديد لانتهاء الصلاحية سيكون قد انقضى بالفعل.",
"deleted": "محذوف",
"keepSearching": "واصل البحث",
"keepSearchingDescription": "تم البحث فقط في الأنشطة الحالية المُحمّلة. اضغط أدناه للبحث في جميع الأنشطة.",
"noResultsFound": "لا توجد نتائج",
"extendFromPreviousExpiry": "تمديد من تاريخ انتهاء الصلاحية السابق (إن أمكن)",
"useInviteExpiryNote": "تنتهي صلاحية الدعوات بشكل افتراضي بعد 90 يوم، ولكن يمكن للمستخدم تجديدها. فعّل هذه الخيار لتعطيل الإحالة بعد الوقت المحدد.",
"noResultsFoundLocally": "تم البحث في السجلات المُحمّلة فقط. يمكنك تحميل المزيد، أو البحث في جميع سجلات الخادم.",
"applyConfigurationAndPolicy": "تطبيق إعدادات/سياسة Jellyfin",
"applyOmbi": "تطبيق ملف تعريف Ombi (إن وُجد)",
"applyJellyseerr": "تطبيق ملف تعريف Jellyseerr (إن وُجد)",
"settingsHiddenDependency": "الإعدادات المطابقة لبحثك مخفية لأنها تعتمد على قيمة إعداد آخر:",
"settingsDependsOn": "{setting}: يعتمد على {dependency}",
"settingsAdvancedMode": "{setting}: يجب تفعيل الإعدادات المتقدمة",
"settingsMaybeUnderAdvanced": "تلميح: قد تجد ما تبحث عنه عند تفعيل الإعدادات المتقدمة.",
"jellyseerrProfile": "ملف تعريف مستخدم Jellyseerr",
"jellyseerrUserDefaultsDescription": "أنشئ مستخدم Jellyseerr وقم بإعداده، ثم اختره أدناه. سيتم تخزين إعداداته/أذوناته وتطبيقها على مستخدمي Jellyseerr الجدد الذين أُنشئوا بواسطة jfa-go عند اختيار ملف التعريف هذا.",
"sortDirection": "اتجاه الفرز",
"searchAllRecords": "بحث/فرز جميع السجلات (على الخادم)",
"backups": "النسخ الاحتياطية",
"backupsCopy": "عند استعادة نسخة احتياطية، سيتم إنشاء نسخة من مجلد \"db\" الأصلي بجواره، تحسبًا لحدوث أي خطأ.",
"backupCanDownload": "يمكنك أيضاً الضغط أدناه لتنزيل النسخة الاحتياطية.",
"sentTo": "مرسلة إلى",
"tasks": "المهام",
"editProfile": "تعديل ملف التعريف",
"editProfileDescription": "لإجراء تغييرات كبيرة، يُنصح بتعديل الإعدادات في Jellyfin/Jellyseerr/Ombi وإعادة إنشاء ملف التعريف، كما يمكنك أيضاً إجراء تغييرات مباشرة هنا. يُرجى توخي الحذر عند التعديل.",
"tasksDescription": "المهام هي إجراءات كبيرة تُنفَّذ دورياً في الخلفية. يمكنك تشغيلها يدوياً هنا إذا أردت.",
"run": "تشغيل",
"addProfileStoreJellyseerr": "إنشاء ملف تعريف Jellyseerr",
"preSignupCard": "بطاقة التعليمات قبل التسجيل",
"preSignupCardDescription": "بطاقة اختيارية تظهر في صفحة التسجيل.",
"byAdmin": "بواسطة المسؤول",
"byUser": "بواسطة المستخدم",
"byJfaGo": "بواسطة jfa-go",
"accountExpired": "انتهت صلاحية حساب: {user}",
"inviteDeleted": "تم حذف دعوة: {invite}",
"inviteExpired": "انتهت صلاحية دعوة: {invite}",
"fromInvite": "عبر دعوة",
"activityID": "مُعرّف النشاط",
"title": "العنوان",
"usersMentioned": "المستخدم المذكور",
"actor": "المسبب",
"actorDescription": "الشيء الذي تسبب في هذا الإجراء. \"user\"/\"admin\"/\"daemon\" أو اسم مستخدم.",
"accountDisabledFilter": "حساب تم تعطيله",
"accountEnabledFilter": "حساب تم تفعيله",
"accountCreationFilter": "حساب أُنشئ",
"accountDeletionFilter": "حساب حُذِف",
"totalRecords": "{n} سجل بالمجمل",
"noMoreResults": "لا توجد نتائج أخرى.",
"loadMore": "تحميل المزيد",
"loadAll": "تحميل الكل",
"contactLinkedFilter": "وسيلة اتصال رُبِطت",
"contactUnlinkedFilter": "وسيلة اتصال أُزيلت",
"passwordChangeFilter": "كلمة مرور تغيّرت",
"passwordResetFilter": "كلمة مرور أُعيد تعيينها",
"inviteCreatedFilter": "دعوة أُنشئت",
"inviteDeletedFilter": "دعوة حُذِفت/انتهت",
"loadedRecords": "{n} محمّل",
"shownRecords": "{n} معروض",
"selectedRecords": "{n} محدد",
"allMatchingSelected": "جميع النتائج المطابقة محددة.",
"allLoadedSelected": "جميع النتائج المطابقة المحمّلة محددة. اضغط مرة أخرى لتحميل الكل.",
"restartRequired": "يلزم إعادة التشغيل",
"syntax": "الصياغة",
"syntaxDescription": "المتغيرات يشار إليها بـ {variable}. يمكن للعبارات الشرطية (if) تقييم مدى الصحة (مثل {ifTruth}) أو إجراء مقارنات بسيطة (مثل {ifCompare})",
"postSignupCard": "بطاقة التعليمات بعد التسجيل",
"postSignupCardDescription": "البطاقة التي تظهر للمستخدم بعد التسجيل. تَستبدل \"رسالة النجاح\". يستبدلها إعداد \"Auto redirect on success\".",
"buildTags": "وسوم النُسخة",
"loginNotAdmin": "لست المسؤول؟",
"referrer": "المُحيل (إحالة)",
"accountResetPassword": "{user} أعاد تعيين كلمة مروره",
"accountChangedPassword": "{user} غير كلمة مروره",
"accountCreated": "تم إنشاء حساب: {user}",
"accountReEnabled": "تم إعادة تفعيل حساب: {user}",
"accountDeleted": "تم حذف حساب: {user}",
"accountDisabled": "تم تعطيل حساب: {user}",
"accountUnlinked": "{user}: أزال {contactMethod}",
"accountLinked": "{user}: ربط {contactMethod}",
"backupsDescription": "يمكن إجراء نسخ احتياطية لقاعدة البيانات أو استعادتها أو تنزيلها من هنا.",
"backupsFormatNote": "لن يتم عرض سوى النسخ الاحتياطية المسمّاة بالصيغة الأساسية. لاستخدام أي صيغة أخرى، قم برفع النسخة الاحتياطية يدوياً.",
"backupDownloadRestore": "تنزيل / استعادة",
"backupUpload": "رفع واستعادة نسخة احتياطية",
"backupDownload": "تنزيل النسخة الاحتياطية",
"backupRestore": "استعادة النسخة الاحتياطية",
"backupNow": "إنشاء نسخة احتياطية",
"backupCreated": "تم إنشاء النسخة الاحتياطية",
"backupCanBeFound": "يمكن العثور على النسخة الاحتياطية على الخادم في {filepath}.",
"required": "مطلوب",
"searchAll": "بحث/فرز الكل",
"accountWillExpire": "ستنتهي صلاحية الحساب في {date}.",
"expirationBasedOn": "التاريخ المحدد مبني على أول مستخدم.",
"inviteCreated": "تم إنشاء دعوة: {invite}",
"userDeleted": "تم حذف المستخدم.",
"userDisabled": "تم تعطيل المستخدم"
},
"notifications": {
"changedEmailAddress": "",
"userCreated": "",
"createProfile": "",
"saveSettings": "",
"saveEmail": "",
"sentAnnouncement": "",
"savedAnnouncement": "",
"setOmbiProfile": "",
"updateApplied": "",
"updateAppliedRefresh": "",
"telegramVerified": "",
"accountConnected": "",
"errorSettingsAppliedNoHomescreenLayout": "",
"errorHomescreenAppliedNoSettings": "",
"errorSettingsFailed": "",
"errorSaveEmail": "",
"errorBlankFields": "",
"errorDeleteProfile": "",
"errorLoadProfiles": "",
"errorCreateProfile": "",
"errorSetDefaultProfile": "",
"errorLoadUsers": "",
"errorLoadSettings": "",
"errorSetOmbiProfile": "",
"errorLoadOmbiUsers": "",
"errorChangedEmailAddress": "",
"errorFailureCheckLogs": "",
"errorPartialFailureCheckLogs": "",
"errorUserCreated": "",
"errorSendWelcomeEmail": "",
"errorApplyUpdate": "",
"errorCheckUpdate": "",
"updateAvailable": "",
"noUpdatesAvailable": ""
"changedEmailAddress": "تم تغيير عنوان البريد الإلكتروني لـ {n}.",
"userCreated": "تم إنشاء المستخدم {n}.",
"createProfile": "تم إنشاء ملف التعريف {n}.",
"saveSettings": "تم حفظ الإعدادات",
"saveEmail": "تم حفظ البريد الإلكتروني.",
"sentAnnouncement": "تم إرسال الإعلان.",
"savedAnnouncement": "تم حفظ الإعلان.",
"setOmbiProfile": "تم تخزين ملف تعريف Ombi.",
"updateApplied": "تم تطبيق التحديث، يرجى إعادة التشغيل.",
"updateAppliedRefresh": "تم تطبيق التحديث، يرجى تحديث الصفحة.",
"telegramVerified": "تم تأكيد حساب Telegram.",
"accountConnected": "تم ربط الحساب.",
"errorSettingsAppliedNoHomescreenLayout": "تم تطبيق الإعدادات، ولكن ربما فشل تطبيق مخطط الصفحة الرئيسية.",
"errorHomescreenAppliedNoSettings": "تم تطبيق مخطط الصفحة الرئيسية، ولكن ربما فشل تطبيق الإعدادات.",
"errorSettingsFailed": "فشل التطبيق.",
"errorSaveEmail": "فشل حفظ البريد الإلكتروني.",
"errorBlankFields": "تُركت الحقول فارغة",
"errorDeleteProfile": "فشل حذف ملف التعريف {n}",
"errorLoadProfiles": "فشل تحميل ملفات التعريف.",
"errorCreateProfile": "فشل إنشاء ملف التعريف {n}",
"errorSetDefaultProfile": "فشل تعيين ملف التعريف الافتراضي.",
"errorLoadUsers": "فشل تحميل المستخدمين.",
"errorLoadSettings": "فشل تحميل الإعدادات.",
"errorSetOmbiProfile": "فشل تخزين ملف تعريف Ombi.",
"errorLoadOmbiUsers": "فشل تحميل مستخدمي Ombi.",
"errorChangedEmailAddress": "تعذر تغيير عنوان البريد الإلكتروني لـ {n}.",
"errorFailureCheckLogs": "فشل (تحقق من لوحة التحكم/السجلات)",
"errorPartialFailureCheckLogs": "فشل جزئي (تحقق من لوحة التحكم/السجلات)",
"errorUserCreated": "فشل إنشاء المستخدم {n}.",
"errorSendWelcomeEmail": "فشل إرسال رسالة الترحيب (تحقق من لوحة التحكم/السجلات)",
"errorApplyUpdate": "فشل تطبيق التحديث، حاول يدوياً.",
"errorCheckUpdate": "فشل التحقق من التحديثات.",
"updateAvailable": "يتوفر تحديث جديد، تحقق من الإعدادات.",
"noUpdatesAvailable": "لا توجد تحديثات جديدة متاحة.",
"errorInviteNoLongerExists": "الدعوة لم تعد موجودة.",
"pathCopied": "تم نسخ المسار الكامل إلى الحافظة.",
"errorInvalidAddress": "عنوان/اسم غير صالح",
"referralsEnabled": "تم تفعيل الإحالات.",
"activityDeleted": "تم حذف النشاط.",
"errorLoadProfile": "فشل تحميل ملف التعريف.",
"errorCheckLogs": "تحقق من لوحة التحكم/السجلات",
"errorInvalidJSON": "JSON غير صالح.",
"runTask": "تم تشغيل المهمة.",
"errorMultiUser": "تم العثور على عدة مستخدمين متطابقين",
"errorNoUser": "لم يتم العثور على مستخدم مطابق",
"errorInviteNotFound": "الدعوة غير موجودة.",
"errorNoReferralTemplate": "لا يحتوي ملف التعريف على قالب إحالة، أضف واحداً في الإعدادات.",
"errorLoadActivities": "فشل تحميل الأنشطة.",
"errorInvalidDate": "التاريخ غير صالح.",
"savedProfile": "تم تخزين تغييرات ملف التعريف.",
"errorSavedProfile": "فشل حفظ ملف التعريف {n}"
},
"quantityStrings": {
"modifySettingsFor": {
"singular": "",
"plural": ""
"singular": "تغيير الإعدادات لمستخدم واحد",
"plural": "تغيير الإعدادات لـ {n} من المستخدمين"
},
"deleteNUsers": {
"singular": "",
"plural": ""
"singular": "حذف مستخدم واحد",
"plural": "حذف {n} من المستخدمين"
},
"disableUsers": {
"singular": "",
"plural": ""
"singular": "تعطيل مستخدم واحد",
"plural": "تعطيل {n} من المستخدمين"
},
"reEnableUsers": {
"singular": "",
"plural": ""
"singular": "إعادة تفعيل مستخدم واحد",
"plural": "إعادة تفعيل {n} من المستخدمين"
},
"addUser": {
"singular": "",
"plural": ""
"singular": "إضافة مستخدم",
"plural": "إضافة مستخدمين"
},
"deleteUser": {
"singular": "",
"plural": ""
"singular": "حذف المستخدم",
"plural": "حذف المستخدمين"
},
"deletedUser": {
"singular": "",
"plural": ""
"singular": "تم حذف مستخدم واحد.",
"plural": "تم حذف {n} من المستخدمين."
},
"disabledUser": {
"singular": "",
"plural": ""
"singular": "تم تعطيل مستخدم واحد.",
"plural": "تم تعطيل {n} من المستخدمين."
},
"enabledUser": {
"singular": "",
"plural": ""
"singular": "تم تفعيل مستخدم واحد.",
"plural": "تم تفعيل {n} من المستخدمين."
},
"announceTo": {
"singular": "",
"plural": ""
"singular": "الإعلان إلى مستخدم واحد",
"plural": "الإعلان إلى {n} من المستخدمين"
},
"appliedSettings": {
"singular": "",
"plural": ""
"singular": "تم تطبيق الإعدادات على مستخدم واحد.",
"plural": "تم تطبيق الإعدادات على {n} من المستخدمين."
},
"extendExpiry": {
"singular": "",
"plural": ""
"singular": "تمديد مدة الصلاحية لمستخدم واحد",
"plural": "تمديد مدة الصلاحية لـ {n} من المستخدمين"
},
"setExpiry": {
"singular": "",
"plural": ""
"singular": "تعيين مدة الصلاحية لمستخدم واحد",
"plural": "تعيين مدة الصلاحية لـ {n} من المستخدمين"
},
"extendedExpiry": {
"singular": "",
"plural": ""
"singular": "تم تمديد مدة الصلاحية لمستخدم واحد.",
"plural": "تم تمديد مدة الصلاحية لـ {n} من المستخدمين."
},
"enableReferralsFor": {
"singular": "تفعيل الإحالات لمستخدم واحد",
"plural": "تفعيل الإحالات لـ {n} من المستخدمين"
}
}
}

View File

@@ -7,6 +7,8 @@
"invite": "Invite",
"accounts": "Accounts",
"activity": "Activity",
"activityFromJF": "Activity (from Jellyfin)",
"activityFromJfa": "Activity (from jfa-go)",
"settings": "Settings",
"inviteMonths": "Months",
"inviteDays": "Days",
@@ -17,6 +19,7 @@
"warning": "Warning",
"inviteInfiniteUsesWarning": "invites with infinite uses can be used abusively",
"inviteSendToEmail": "Send to",
"sentTo": "Sent to",
"create": "Create",
"apply": "Apply",
"select": "Select",
@@ -39,11 +42,15 @@
"commitNoun": "Commit",
"newUser": "New User",
"profile": "Profile",
"editProfile": "Edit profile",
"editProfileDescription": "For large changes, it is recommended you modify settings in Jellyfin/Jellyseerr/Ombi and re-generate the profile, but you can also make direct changes here. Please use caution when editing.",
"unknown": "Unknown",
"label": "Label",
"userLabel": "User Label",
"userLabelDescription": "Label to apply to users created with this invite.",
"logs": "Logs",
"tasks": "Tasks",
"tasksDescription": "Tasks are large actions that may be run periodically in the background. You can manually trigger them here if you wish.",
"announce": "Announce",
"templates": "Templates",
"subject": "Subject",
@@ -56,7 +63,10 @@
"unlink": "Unlink Account",
"deleted": "Deleted",
"disabled": "Disabled",
"run": "Run",
"sendPWR": "Send Password Reset",
"noActivityFound": "No activity found",
"noActivityFoundLocally": "If this account hasn't been used in a while, you might find older entries on Jellyfin directly.",
"noResultsFound": "No Results Found",
"noResultsFoundLocally": "Only loaded records were searched. You can load more, or perform the search over all records on the server.",
"keepSearching": "Keep Searching",
@@ -115,6 +125,7 @@
"addProfileDescription": "Create a Jellyfin user and configure it, then select it below. When this profile is applied to an invite, new users will be created with the settings.",
"addProfileNameOf": "Profile Name",
"addProfileStoreHomescreenLayout": "Store homescreen layout",
"addProfileStoreJellyseerr": "Create Jellyseerr profile",
"inviteNoUsersCreated": "None yet!",
"inviteUsersCreated": "Created users",
"inviteNoProfile": "No Profile",
@@ -149,6 +160,8 @@
"userPagePage": "User Page: Page",
"postSignupCard": "Post-signup help card",
"postSignupCardDescription": "Card shown to user after signing up. Overrides \"Success Message\". Overriden by \"Auto redirect on success\" setting.",
"preSignupCard": "Pre-signup help card",
"preSignupCardDescription": "Optional card shown on the sign-up page.",
"buildTime": "Build Time",
"builtBy": "Built By",
"buildTags": "Build Tags",
@@ -213,7 +226,15 @@
"wikiPage": "Wiki Page",
"wiki": "Wiki",
"restartRequired": "Restart required",
"required": "Required"
"required": "Required",
"syntax": "Syntax",
"syntaxDescription": "Variables denoted as {variable}. If statements can evaluate truthfulness (e.g. {ifTruth}) or make basic comparisons (e.g. {ifCompare})",
"info": "Info",
"debug": "Debug",
"warn": "Warn",
"error": "Error",
"fatal": "Fatal",
"severity": "Severity"
},
"notifications": {
"pathCopied": "Full path copied to clipboard.",
@@ -241,6 +262,7 @@
"errorBlankFields": "Fields were left blank",
"errorDeleteProfile": "Failed to delete profile {n}",
"errorLoadProfiles": "Failed to load profiles.",
"errorLoadProfile": "Failed to load profile.",
"errorCreateProfile": "Failed to create profile {n}",
"errorSavedProfile": "Failed to save profile {n}",
"errorSetDefaultProfile": "Failed to set default profile.",
@@ -250,6 +272,7 @@
"errorLoadOmbiUsers": "Failed to load ombi users.",
"errorChangedEmailAddress": "Couldn't change email address of {n}.",
"errorFailureCheckLogs": "Failed (check console/logs)",
"errorCheckLogs": "Check console/logs",
"errorPartialFailureCheckLogs": "Partial failure (check console/logs)",
"errorUserCreated": "Failed to create user {n}.",
"errorSendWelcomeEmail": "Failed to send welcome message (check console/logs)",
@@ -258,8 +281,13 @@
"errorNoReferralTemplate": "Profile doesn't contain referral template, add one in settings.",
"errorLoadActivities": "Failed to load activities.",
"errorInvalidDate": "Date is invalid.",
"errorInvalidJSON": "Invalid JSON.",
"updateAvailable": "A new update is available, check settings.",
"noUpdatesAvailable": "No new updates available."
"noUpdatesAvailable": "No new updates available.",
"runTask": "Triggered task.",
"errorMultiUser": "Multiple matching users found",
"errorNoUser": "No matching user found",
"errorInvalidAddress": "Invalid address/name"
},
"quantityStrings": {
"modifySettingsFor": {

View File

@@ -5,60 +5,67 @@
"strings": {
"username": "اسم المستخدم",
"password": "كلمة المرور",
"emailAddress": "البريد الالكتروني",
"emailAddress": "عنوان البريد الإلكتروني",
"name": "الاسم",
"submit": "ادخال",
"success": "نجاح",
"continue": "اكمل",
"submit": "إرسال",
"success": "تم",
"continue": "متابعة",
"error": "خطأ",
"copy": "نسخ",
"time24h": "توقيت 24 ساعة",
"time12h": "توقيت 12 ساعة",
"linkTelegram": ابط تلغرام",
"contactTelegram": "التواصل عبر التلغرام",
"linkDiscord": ابط الدسكورد",
"linkTelegram": "ربط Telegram",
"contactTelegram": "التواصل عبر Telegram",
"linkDiscord": "ربط Discord",
"linkMatrix": "ربط Matrix",
"contactDiscord": "التواصل عبر الدسكورد",
"theme": "القالب",
"contactDiscord": "التواصل عبر Discord",
"theme": "السمة",
"refresh": "تحديث",
"required": "مطلوب",
"login": "تسجيل الدخول",
"admin": "المسؤول",
"reEnable": "اعادة تفعيل",
"disable": جميد",
"reEnable": "إعادة تفعيل",
"disable": عطيل",
"accountStatus": "حالة الحساب",
"notSet": "لم تحدد",
"expiry": "انتهاء الصلاحية",
"add": "اضافة",
"add": "إضافة",
"edit": "تعديل",
"delete": "حذف",
"myAccount": "حسابي",
"disabled": "معطل",
"enabled": "مفعل",
"send": "ارسال",
"disabled": "معطّل",
"enabled": "مفعّل",
"send": "إرسال",
"copied": "تم النسخ",
"contactEmail": "التواصل عبر البريد الالكتروني",
"contactEmail": "التواصل عبر البريد",
"logout": "تسجيل الخروج",
"contactMethods": "وسيلة التواصل"
"contactMethods": "وسائل الاتصال",
"referrals": "الإحالات",
"inviteRemainingUses": "الاستخدامات المتبقية",
"internal": "داخلي",
"external": "خارجي",
"sent": "تم إرساُلها",
"failed": "فشل"
},
"notifications": {
"errorUnknown": "خطأ غير معروف.",
"error401Unauthorized": "غير مخول. حاول تحديث الصفحة.",
"errorSaveSettings": "لا يمكن حفظ الاعدادات.",
"errorLoginBlank": "اسم المستخدم و/أو كلمة المرور لم يتم ادخالها.",
"errorConnection": "لا يمكن الاتصال بـالبرنامج."
"error401Unauthorized": "غير مخوّل. حاول تحديث الصفحة.",
"errorSaveSettings": "تعذر حفظ الإعدادات.",
"errorLoginBlank": "اسم المستخدم و/أو كلمة المرور تُرِكا فارغَين.",
"errorConnection": "تعذر الاتصال بـ jfa-go.",
"errorSpecialSymbols": "لا يمكن أن يحتوي الحقل على رموز خاصة."
},
"quantityStrings": {
"year": {
"singular": "{n} سنة",
"singular": "سنة",
"plural": "{n} سنوات"
},
"month": {
"singular": "{n} شهر",
"singular": "شهر",
"plural": "{n} أشهر"
},
"day": {
"singular": "{n} يوم",
"singular": "يوم",
"plural": "{n} أيام"
}
}

View File

@@ -3,12 +3,14 @@
"name": "English (US)"
},
"strings": {
"language": "Language",
"username": "Username",
"password": "Password",
"emailAddress": "Email Address",
"name": "Name",
"submit": "Submit",
"send": "Send",
"sent": "Sent",
"success": "Success",
"continue": "Continue",
"error": "Error",
@@ -43,7 +45,13 @@
"referrals": "Referrals",
"inviteRemainingUses": "Remaining uses",
"internal": "Internal",
"external": "External"
"external": "External",
"failed": "Failed",
"details": "Details",
"type": "Type",
"other": "Other",
"back": "Back",
"none": "None"
},
"notifications": {
"errorLoginBlank": "The username and/or password were left blank.",

View File

@@ -1,6 +1,6 @@
{
"meta": {
"name": "انگلیسی"
"name": "انگلیسی (FA)"
},
"strings": {
"username": "نام کاربری",

View File

@@ -3,64 +3,64 @@
"name": "العربية (AR)"
},
"strings": {
"ifItWasNotYou": "اذا لم يكن هذا انت، الرجاء تجاهل هذا.",
"helloUser": "مرحباً {username}،",
"ifItWasNotYou": "اذا لم يكن هذا أنت، الرجاء تجاهل هذه الرسالة.",
"helloUser": "أهلاً {username}،",
"reason": "السبب"
},
"userCreated": {
"name": "إنشاء حساب",
"title": "ملاحظة: تم إنشاء الحساب",
"aUserWasCreated": "تم إنشاء الحساب بواسطة الرمز {code}.",
"name": "إنشاء مستخدم",
"title": "إشعار: تم إنشاء المستخدم",
"aUserWasCreated": "تم إنشاء مستخدم باستخدام الرمز {code}.",
"time": "الوقت",
"notificationNotice": "ملاحظة: الرسائل التذكيرية يمكن تعديلها في لوحة التحكم."
},
"inviteExpiry": {
"name": "انتهاء صلاحية الدعوة",
"title": "ملاحظة: انتهت صلاحية الدعوة",
"title": "إشعار: انتهت صلاحية الدعوة",
"inviteExpired": "انتهت صلاحية الدعوة.",
"expiredAt": "انتهت صلاحية الرمز {code} في {time} .",
"notificationNotice": "ملاحظة: الرسائل التذكيرية يمكن تعديلها في لوحة التحكم."
},
"passwordReset": {
"name": "إعادة تعيين كلمة المرور",
"title": "تم طلب إعادة تعيين كلمة المرور - Jellyfin",
"title": "طلب إعادة تعيين كلمة المرور - Jellyfin",
"someoneHasRequestedReset": "قام شخص ما بطلب إعادة تعيين كلمة المرور مؤخرا.",
"ifItWasYou": "إذا كان هذا انت، أدخل رمز التعريف الشخصي أدناه في الخانة.",
"ifItWasYouLink": "إذا كان هذا انت، اضغط على الرابط أدناه.",
"ifItWasYou": "إذا كان هذا انت، أدخل الرمز أدناه في الخانة.",
"ifItWasYouLink": "إذا كان هذا انت، اضغط الرابط أدناه.",
"codeExpiry": "ستنتهي صلاحية الرمز في {date}، {time} UTC، خلال {expiresInMinutes}.",
"pin": "رمز التعريف الشخصي"
"pin": "الرمز"
},
"userDeleted": {
"name": "حذف المستخدم",
"title": "لقد تم حذف حسابك - Jellyfin",
"name": "حذف مستخدم",
"title": "تم حذف حسابك - Jellyfin",
"yourAccountWasDeleted": "لقد تم حذف حسابك في Jellyfin."
},
"userDisabled": {
"name": "تعطيل المستخدم",
"title": "لقد تم تعطيل حسابك - Jellyfin",
"name": "تعطيل مستخدم",
"title": "تم تعطيل حسابك - Jellyfin",
"yourAccountWasDisabled": "لقد تم تعطيل حسابك."
},
"userEnabled": {
"name": "تفعيل المستخدم",
"title": "لقد تم تفعيل حسابك - Jellyfin",
"name": "تفعيل مستخدم",
"title": "تم تفعيل حسابك - Jellyfin",
"yourAccountWasEnabled": "لقد تم تفعيل حسابك."
},
"inviteEmail": {
"name": "دعوة البريد الإلكتروني",
"name": "بريد الدعوة",
"title": "دعوة - Jellyfin",
"hello": "مرحباً",
"hello": "أهلاً",
"youHaveBeenInvited": "تمت دعوتك إلى Jellyfin.",
"toJoin": "للإنضمام، اتبع الرابط أدناه.",
"inviteExpiry": "ستنتهي صلاحية الدعوة في {date} {time}، خلال {expiresInMinutes}، اتخذ اجراءاً.",
"linkButton": "قم بإعداد حسابك"
},
"welcomeEmail": {
"name": "مرحباً",
"name": "الترحيب",
"title": "مرحباً في Jellyfin",
"welcome": "مرحباً في Jellyfin!",
"youCanLoginWith": "يمكنك تسجيل الدخول بإستخدام المعلومات أدناه",
"yourAccountWillExpire": "ستنتهي صلاحية حسابك في {date}.",
"jellyfinURL": "رابط"
"jellyfinURL": "الرابط"
},
"emailConfirmation": {
"name": "بريد التحقق",
@@ -69,9 +69,21 @@
"confirmEmail": "تأكيد البريد الإلكتروني"
},
"userExpired": {
"name": "انتهاء صلاحية المستخدم",
"name": "انتهاء صلاحية مستخدم",
"title": "انتهت صلاحية حسابك - Jellyfin",
"yourAccountHasExpired": "انتهت صلاحية حسابك.",
"contactTheAdmin": "تواصل مع المشرفين للمزيد من المعلومات."
},
"expiryReminder": {
"name": "تذكير انتهاء الصلاحية",
"title": "تذكير: ستنتهي صلاحية حسابك قريباً - Jellyfin",
"yourAccountIsDueToExpire": "ستنتهي صلاحية حسابك خلال {expiresIn}، في{date} {time}."
},
"userExpiryAdjusted": {
"yourExpiryWasAdjusted": "تم تغيير تاريخ انتهاء صلاحية حسابك.",
"name": "تغيير مدة انتهاء الصلاحية",
"title": "تغيرت مدة صلاحية حسابك - Jellyfin",
"ifPreviouslyDisabled": "إذا تم تعطيل حسابك مسبقاً، فمن الممكن أنه قد تم تفعيله.",
"newExpiry": "ستنتهي صلاحية حسابك في: {date}."
}
}

View File

@@ -10,70 +10,79 @@
"username": "اسم المستخدم",
"password": "كلمة المرور",
"reEnterPassword": "تأكيد كلمة المرور",
"reEnterPasswordInvalid": "كلمة المرور غير مطابقة.",
"reEnterPasswordInvalid": "كلمات المرور غير متطابقة.",
"createAccountButton": "إنشاء الحساب",
"passwordRequirementsHeader": "متطلبات كلمة المرور",
"successHeader": "تم!",
"confirmationRequired": "مطلوب تأكيد البريد الإلكتروني",
"confirmationRequiredMessage": "يرجى التحقق من صندوق البريد الإلكتروني الخاص بك للتحقق من عنوانك.",
"yourAccountIsValidUntil": "سيكون حسابك ساري المفعول حتى {date}.",
"sendPIN": "أرسل رقم التعريف الشخصي أدناه إلى البوت ، ثم عُد إلى هنا لربط حسابك.",
"sendPINDiscord": "اكتب {command} في {server_channel} على Discord ، ثم أرسل رقم التعريف الشخصي أدناه.",
"matrixEnterUser": "أدخل معرف المستخدم الخاص بك، واضغط على إرسال، وسيتم إرسال رمز PIN إليك. أدخله هنا للمتابعة.",
"oldPassword": "كلمة المرور السابقة",
"confirmationRequiredMessage": "يرجى التحقق من بريدك الإلكتروني لتأكيد العنوان.",
"yourAccountIsValidUntil": "سيكون حسابك صالحاً حتى {date}.",
"sendPIN": "أرسل الرمز أدناه إلى البوت، ثم عُد إلى هنا لربط حسابك.",
"sendPINDiscord": "اكتب {command} في {server_channel} في Discord، ثم أرسل الرمز أدناه.",
"matrixEnterUser": "أدخل مُعرّف المستخدم الخاص بك، واضغط على إرسال، وسيتم إرسال رمز إليك. أدخله هنا للمتابعة.",
"oldPassword": "كلمة المرور القديمة",
"newPassword": "كلمة المرور الجديدة",
"joinTheServer": "انضم إلى السيرفر:",
"editContactMethod": حرير طريقة الاتصال",
"customMessagePlaceholderHeader": حرير هذه البطاقة",
"joinTheServer": "انضم إلى الخادم:",
"editContactMethod": غيير وسيلة الاتصال",
"customMessagePlaceholderHeader": خصيص هذه البطاقة",
"resetPassword": "إعادة تعيين كلمة المرور",
"resetSent": "تم إرسال إعادة التعيين.",
"resetSentDescription": "في حال وجود حساب يملك اسم المستخدم/طريقة الاتصال المحددة، فسيتم إرسال رابط إعادة تعيين كلمة المرور عبر جميع طرق الاتصال المتاحة.ستنتهي صلاحية الرمز خلال 30 دقيقة.",
"resetSentDescription": "في حال وجود حساب يملك اسم المستخدم/وسيلة الاتصال المحددة، فسيتم إرسال رابط إعادة تعيين كلمة المرور عبر جميع وسائل الاتصال المتاحة. ستنتهي صلاحية الرمز خلال 30 دقيقة.",
"changePassword": "تغيير كلمة المرور",
"welcomeUser": "مرحباً، {user}!",
"addContactMethod": "إضافة طريقة اتصال",
"customMessagePlaceholderContent": "اضغط على زر تحرير صفحة المستخدم في الإعدادات لتخصيص هذه البطاقة، أو أظهر واحدة على صفحة تسجيل الدخول، ولا تقلق، لا يستطيع المستخدم رؤيتها.",
"userPageSuccessMessage": "تستطيع رؤية وتحرير التفاصيل حول حسابك لاحقاً في صفحة {myAccount}.",
"addContactMethod": "أضف وسيلة اتصال",
"customMessagePlaceholderContent": "اضغط على زر التعديل في صفحة المستخدم في الإعدادات لتخصيص هذه البطاقة، أو أظهر واحدة على صفحة تسجيل الدخول، ولا تقلق، لا يستطيع المستخدم رؤيتها.",
"userPageSuccessMessage": "يمكنك الاطلاع على تفاصيل حسابك وتغييرها لاحقاً على صفحة {myAccount}.",
"resetPasswordThroughJellyfin": "لإعادة تعيين كلمة المرور، قم بزيارة {jfLink} واضغط على زر \"نسيت كلمة المرور\".",
"resetPasswordThroughLink": "لإعادة تعيين كلمة المرور، أدخل اسم المستخدم، البريد الإلكتروني، أو اسم مستخدم لطريقة اتصال مرتبطة، ثم أرسل. سيتم إرسال رابط لإعادة تعيين كلمة المرور."
"resetPasswordThroughLink": "لإعادة تعيين كلمة المرور، أدخل اسم المستخدم، عنوان البريد الإلكتروني، أو اسم مستخدم لوسيلة اتصال مربوطة، ثم أرسل. سيتم إرسال رابط لإعادة تعيين كلمة المرور.",
"resetPasswordThroughLinkEnd": "ثم اضغط على إرسال. سيتم إرسال رابط لإعادة تعيين كلمة مرورك.",
"referralsDescription": "ادعُ أصدقاءك وعائلتك إلى Jellyfin باستخدام هذا الرابط. عُد إلى هنا للحصول على رابط جديد إذا انتهت صلاحيته.",
"resetPasswordThroughLinkStart": "لإعادة تعيين كلمة مرورك، أدخل أحد الخيارات التالية:",
"resetPasswordContactMethod": "اسم المستخدم لأي وسيلة اتصال مرتبطة بحسابك",
"copyReferral": "نسخ الرابط",
"invitedBy": "تمت دعوتك من قبل المستخدم {user}.",
"resetPasswordUsername": "اسم مستخدم Jellyfin الخاص بك",
"resetPasswordEmail": "عنوان بريدك الإلكتروني",
"referralsWithExpiryDescription": "ادعُ أصدقاءك وعائلتك إلى Jellyfin باستخدام هذا الرابط. سيتم تعطيل الرابط بعد انتهاء صلاحيته."
},
"notifications": {
"errorUserExists": "المستخدم موجود مسبقا.",
"errorInvalidCode": "رمز دعوة غير صالح.",
"errorTelegramVerification": "مطلوب التحقق من تيليجرام.",
"errorDiscordVerification": "مطلوب التحقق من الدسكورد.",
"errorMatrixVerification": "مطلوب التحقق من Matrix.",
"errorInvalidPIN": "رقم التعريف الشخصي غير صالح.",
"errorTelegramVerification": "مطلوب تأكيد Telegram.",
"errorDiscordVerification": "مطلوب تأكيد Discord.",
"errorMatrixVerification": "مطلوب تأكيد Matrix.",
"errorInvalidPIN": "الرمز غير صالح.",
"errorUnknown": "خطأ غير معروف.",
"errorNoEmail": "البريد الإلكتروني مطلوب.",
"errorCaptcha": "كلمة التحقق غير صحيحة.",
"errorCaptcha": "كلمة التحقق خاطئة.",
"errorPassword": "تحقق من متطلبات كلمة المرور.",
"errorNoMatch": "كلمات المرور غير متطابقة.",
"verified": "تم التحقق من الحساب.",
"errorAccountLinked": "الحساب قيد الاستخدام.",
"errorEmailLinked": "البريد الإلكتروني قيد الاستخدام.",
"errorOldPassword": "كلمة المرور القديمة غير صحيحة.",
"verified": "تم تأكيد الحساب.",
"errorAccountLinked": "الحساب مستخدم بالفعل.",
"errorEmailLinked": "البريد مستخدم بالفعل.",
"errorOldPassword": "كلمة المرور القديمة خاطئة.",
"passwordChanged": "تم تغيير كلمة المرور."
},
"validationStrings": {
"length": {
"singular": "يجب أن يتألف من {n} حرف على الأقل",
"singular": "يجب ألا يقل عدد الأحرف عن {n}",
"plural": "يجب ألا يقل عدد الأحرف عن {n}"
},
"uppercase": {
"singular": "يجب أن تحتوي على {n} حرف كبير على الأقل",
"singular": "يجب ألا يقل عدد الأحرف الكبيرة عن {n}",
"plural": "يجب ألا يقل عدد الأحرف الكبيرة عن {n}"
},
"lowercase": {
"singular": "يجب أن يتألف من {n} حرف صغير على الأقل",
"singular": "يجب ألا يقل عدد الأحرف الصغيرة عن {n}",
"plural": "يجب ألا يقل عدد الأحرف الصغيرة عن {n}"
},
"number": {
"singular": "يجب أن يحتوي على {n} رقم على الأقل",
"plural": "يجب أن يحتوي على {n} رقم على الأقل"
"singular": "يجب ألا يقل عدد الأرقام عن {n}",
"plural": "يجب ألا يقل عدد الأرقام عن {n}"
},
"special": {
"singular": "يجب أن يتألف من {n} حرف خاص على الأقل",
"plural": "يجب أن يتألف من {n} حرف خاص على الأقل"
"singular": "يجب ألا يقل عدد الأحرف الخاصة عن {n}",
"plural": "يجب ألا يقل عدد الأحرف الخاصة عن {n}"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"meta": {
"name": "کوردی سۆرانی"
"name": "کوردی سۆرانی (CKB)"
},
"strings": {
"pageTitle": "دروستکردنی هەژماری Jellyfin",

View File

@@ -1,6 +1,6 @@
{
"meta": {
"name": "انگلیسی"
"name": "انگلیسی (FA)"
},
"strings": {
"pageTitle": "ساختن حساب کاربری",

View File

@@ -4,12 +4,12 @@
},
"strings": {
"passwordReset": "إعادة تعيين كلمة المرور",
"reset": "إعادة ضبط",
"reset": "إعادة التعيين",
"resetFailed": "فشل إعادة تعيين كلمة المرور",
"tryAgain": "حاول مرة اخرى.",
"youCanLogin": "يمكنك الآن تسجيل الدخول باستخدام الرمز أدناه ككلمة المرور الخاصة بك.",
"youCanLoginOmbi": "يمكنك الآن تسجيل الدخول باستخدام الرمز أدناه ككلمة المرور الخاصة بك.",
"youCanLoginPassword": "يمكنك الآن تسجيل الدخول باستخدام الرمز أدناه ككلمة المرور الخاصة بك.",
"tryAgain": "حاول مرة أُخرى.",
"youCanLogin": "يمكنك الآن الدخول باستخدام الرمز أدناه ككلمة مرورك.",
"youCanLoginOmbi": "يمكنك الآن الدخول إلى Jellyfin وOmbi باستخدام الرمز أدناه ككلمة مرورك.",
"youCanLoginPassword": "يمكنك الآن الدخول باستخدام كلمة مرورك الجديدة. اضغط أدناه للانتقال إلى Jellyfin.",
"changeYourPassword": "تأكد من تغيير كلمة المرور الخاصة بك بعد تسجيل الدخول.",
"enterYourPassword": "أدخل كلمة المرور الجديدة أدناه."
}

View File

@@ -1,6 +1,6 @@
{
"meta": {
"name": "انگلیسی"
"name": "انگلیسی (FA)"
},
"strings": {
"passwordReset": "تنظیم مجدد رمز عبور",

View File

@@ -8,8 +8,8 @@
"back": "الخلف",
"optional": "اختياري",
"serverType": "نوع الخادم",
"disabled": "معطل",
"enabled": "مفعل",
"disabled": "معطّل",
"enabled": "مفعّل",
"port": "المنفذ",
"message": "الرسالة",
"serverAddress": "عنوان الخادم",
@@ -18,135 +18,164 @@
"apiKey": "مفتاح API",
"error": "خطأ",
"errorInvalidUserPass": "اسم مستخدم/كلمة مرور خاطئة.",
"errorNotAdmin": "",
"errorUserDisabled": "",
"error404": "",
"errorConnectionRefused": "",
"errorUnknown": ""
"errorNotAdmin": "لا يُسمح للمستخدم بإدارة الخادم.",
"errorUserDisabled": "قد يكون المستخدم معطلاً.",
"error404": "404، تحقق من عنوان URL الداخلي.",
"errorConnectionRefused": "رُفض الاتصال.",
"errorUnknown": "خطأ غير معروف، تحقق من سجلات التطبيق.",
"errorProxy": "إعدادات الوكيل غير صالحة."
},
"startPage": {
"welcome": "",
"pressStart": "",
"httpsNotice": "",
"start": ""
"welcome": "مرحباً!",
"pressStart": "ستحتاج إلى القيام ببعض الخطوات لإعداد jfa-go. اضغط على \"ابدأ\" للمتابعة.",
"httpsNotice": "تأكد من الاتصال بهذه الصفحة عبر HTTPS أو على شبكة خاصة.",
"start": "ابدأ"
},
"endPage": {
"finished": "",
"finished": "انتهينا!",
"restartMessage": "",
"refreshPage": ""
"refreshPage": "تحديث",
"moreFeatures": "يمكنك العثور على المزيد من الميزات مثل بوتات Discord/Telegram/Matrix ورسائل ترميز Markdown المخصصة في الإعدادات، لذا تأكد من تصفحها.",
"restartReload": "اضغط أدناه لإعادة التشغيل، ثم قم بالدخول إلى jfa-go من أحد عناوين URL الداخلية/الخارجية المحددة.",
"ifFailedLoad": "إذا لم يتم تحميل الصفحة، فتحقق من سجلات التطبيق بحثاً عن أي أدلة تشير إلى السبب."
},
"language": {
"title": "",
"description": "",
"defaultAdminLang": "",
"defaultFormLang": "",
"defaultEmailLang": ""
"title": "اللغة",
"description": "تتوفر ترجمات تطوعية لمعظم أجزاء jfa-go. يمكنك اختيار اللغات الافتراضية أدناه، ويمكن للمستخدمين تغييرها إذا رغبوا في ذلك. إذا أردت المساعدة في الترجمة، قم بالتسجيل في {n} للمساهمة!",
"defaultAdminLang": "لغة المسؤول الافتراضية",
"defaultFormLang": "لغة إنشاء الحساب الافتراضية",
"defaultEmailLang": "لغة البريد الافتراضية"
},
"general": {
"title": "",
"listenAddress": "",
"urlBase": "",
"urlBaseNotice": "",
"lightTheme": "",
"darkTheme": "",
"useHTTPS": "",
"httpsPort": "",
"useHTTPSNotice": "",
"pathToCertificate": "",
"pathToKeyFile": ""
"title": "عام",
"listenAddress": "عنوان الاستماع",
"urlBase": "عنوان URL الأساسي",
"urlBaseNotice": "مطلوب فقط في حالة استخدام وكيل عكسي على مجلد فرعي (على سبيل المثال 'jellyf.in/accounts').",
"lightTheme": "فاتح",
"darkTheme": "داكن",
"useHTTPS": "استخدم HTTPS",
"httpsPort": "منفذ HTTPS",
"useHTTPSNotice": "موصى به فقط في حال عدم استخدامك لوكيل عكسي.",
"pathToCertificate": "مسار الشهادة",
"pathToKeyFile": "مسار ملف المفتاح (.key)",
"externalURLNotice": "عنوان URL المستخدم للوصول إلى jfa-go. يُستخدم لإنشاء روابط لأمور مثل إعادة تعيين كلمة المرور. تأكد من تضمين عنوان URL الأساسي أعلاه إذا قمت بتعيينه.",
"externalURL": "عنوان URL الخارجي لـ jfa-go"
},
"updates": {
"title": "",
"description": "",
"updateChannel": "",
"stable": "",
"unstable": ""
"title": "التحديثات",
"description": "تلقي إشعارات عند توفر تحديثات جديدة. سيتحقق jfa-go من {n} كل 30 دقيقة. لا يتم جمع أي عناوين IP أو معلومات شخصية.",
"updateChannel": "قناة التحديثات",
"stable": "مستقرة",
"unstable": "غير مستقرة"
},
"login": {
"title": "",
"description": "",
"authorizeWithJellyfin": "",
"authorizeManual": "",
"adminOnly": "",
"allowAll": "",
"allowAllDescription": "",
"emailNotice": ""
"title": "تسجيل الدخول",
"description": "للوصول إلى صفحة المسؤول، سجّل الدخول بإحدى الطرق التالية:",
"authorizeWithJellyfin": "التفويض باستخدام Jellyfin/Emby: يتم مشاركة تفاصيل تسجيل الدخول مع Jellyfin، مما يسمح بتعدد المستخدمين.",
"authorizeManual": "اسم المستخدم وكلمة المرور: قم بتعيين اسم المستخدم وكلمة المرور يدوياً.",
"adminOnly": "المسؤولون فقط (مستحسن)",
"allowAll": "السماح لجميع مستخدمي Jellyfin بتسجيل الدخول",
"allowAllDescription": "لا يُنصح بذلك، ينبغي السماح للمستخدمين الفرديين بتسجيل الدخول بعد الإعداد.",
"emailNotice": "يمكن استخدام عنوان بريدك الإلكتروني لتلقي الإشعارات.",
"authorizeManualUserPageNotice": "سيؤدي استخدام هذا إلى تعطيل ميزة \"صفحة المستخدم\"."
},
"jellyfinEmby": {
"title": "",
"description": "",
"embyNotice": "",
"internal": "",
"external": "",
"replaceJellyfin": "",
"replaceJellyfinNotice": "",
"addressExternalNotice": "",
"testConnection": ""
"title": "إعداد Jellyfin/Emby",
"description": "يلزم وجود حساب مسؤول لأن واجهة برمجة التطبيقات (API) لا تسمح بإنشاء مستخدمين من خلالها. يجب عليك إنشاء حساب منفصل وتحديد خيار \"اسمح لهذا المستخدم بالتحكم بالخادم\". يمكنك تعطيل كل الخيارات الأخرى. بعد ذلك، أدخل بيانات تسجيل الدخول هنا.",
"embyNotice": "دعم Emby محدود ولا يدعم إعادة تعيين كلمة المرور.",
"internal": "داخلي",
"external": "خارجي",
"replaceJellyfin": "اسم الخادم",
"replaceJellyfinNotice": "عند إدخاله، سيحل هذا الاسم محل أي ذكر لكلمة \"Jellyfin\" في التطبيق.",
"addressExternalNotice": "اتركه فارغاً لاستخدام نفس العنوان.",
"testConnection": "اختبر الاتصال"
},
"ombi": {
"title": "",
"description": "",
"apiKeyNotice": ""
"title": "إعداد Ombi",
"description": "عند الاتصال بـ Ombi، سيتم إنشاء حساب Jellyfin و Ombi عندما ينضم المستخدم عبر jfa-go. بعد الانتهاء من الإعداد، انتقل إلى الإعدادات لتعيين ملف تعريف افتراضي لمستخدمي Ombi الجدد.",
"apiKeyNotice": "تجده في أول قسم لإعدادات Ombi.",
"stabilityWarning": "تحذير: تكامل Ombi غير مستقر، وقد يُسبب مشاكل. يُنصح باستخدام Jellyseerr بدلاً منه. راجع {n} لمزيد من المعلومات."
},
"messages": {
"title": "",
"description": ""
"title": "الرسائل",
"description": "يُمكن لـ jfa-go إرسال عمليات إعادة تعيين كلمة المرور ورسائل مُختلفة عبر البريد الإلكتروني، ،Discord ،Telegram و/أو Matrix. ‏يُمكنك إعداد البريد الإلكتروني أدناه، ويُمكنك ضبط باقي الخيارات لاحقاً في الإعدادات. يُمكنك الاطلاع على التعليمات في {n}. إذا لم تكن بحاجة إلى هذه الميزة، فيُمكنك تعطيلها هنا."
},
"email": {
"title": "",
"description": "",
"method": "",
"useEmailAsUsername": "",
"useEmailAsUsernameNotice": "",
"fromAddress": "",
"senderName": "",
"dateFormat": "",
"dateFormatNotice": "",
"encryption": "",
"mailgunApiURL": ""
"title": "البريد الإلكتروني",
"description": "يُمكن لـ jfa-go إرسال رموز (PIN) لإعادة تعيين كلمة المرور وإرسال إشعارات متنوعة عبر البريد الإلكتروني. يُمكنك الاتصال بخادم SMTP أو استخدام واجهة برمجة التطبيقات (API) لـ{n}.",
"method": "وسيلة الإرسال",
"useEmailAsUsername": "استخدم عناوين البريد الإلكتروني كاسم المستخدم",
"useEmailAsUsernameNotice": "عند التفعيل، فسيقوم المستخدمون الجدد بتسجيل الدخول إلى Jellyfin/Emby باستخدام عنوان بريدهم الإلكتروني بدلاً من اسم المستخدم.",
"fromAddress": "عنوان المرسل",
"senderName": "اسم المرسل",
"dateFormat": "صيغة التاريخ",
"dateFormatNotice": "التاريخ يتبع تنسيق strftime. لمزيد من المعلومات، يرجى زيارة {n}.",
"encryption": "التشفير",
"mailgunApiURL": "رابط API"
},
"notifications": {
"title": "",
"description": ""
"title": "إشعارات المسؤول",
"description": "عند التفعيل، ستتلقى رسالة لكل دعوة عند انتهاء صلاحيتها او عند انشاء مستخدم من خلالها. إذا لم تختر طريقة تسجيل الدخول عبر Jellyfin، فتأكد من تقديم عنوان بريدك الإلكتروني أو أضف وسيلة اتصال أخرى لاحقاً."
},
"welcomeEmails": {
"title": "",
"description": ""
"title": "رسائل الترحيب",
"description": "عند التفعيل، فسيتم إرسال رسالة إلى المستخدمين الجدد تحتوي على عنوان URL لـ Jellyfin/Emby واسم المستخدم الخاص بهم."
},
"inviteEmails": {
"title": "",
"description": ""
"title": "رسائل الدعوة",
"description": "عند التفعيل، يمكنك إرسال الدعوات مباشرةً إلى بريد المستخدم الإلكتروني، أو إلى مستخدم Discord أو Matrix. ولأنك قد تستخدم وكيل عكسي، يجب عليك تحديد عنوان URL الذي تصل إليه الدعوات. اكتب عنوان URL الأساسي، وأضف '/invite'."
},
"passwordResets": {
"title": "",
"description": "",
"pathToJellyfin": "",
"pathToJellyfinNotice": "",
"resetLinks": "",
"resetLinksNotice": "",
"resetLinksLanguage": "",
"setPassword": "",
"setPasswordNotice": ""
"title": "عمليات إعادة تعيين كلمة المرور",
"description": "عندما يحاول مستخدم إعادة تعيين كلمة مروره، يُنشئ Jellyfin ملفاً باسم 'passwordreset-*.json' يحتوي على رمز (PIN). يقرأ jfa-go الملف ويرسل الرمز (PIN) إلى المستخدم. إذا قمت بتفعيل ميزة \"صفحة المستخدم\"، فيمكن أيضاً إجراء إعادة التعيين هناك، باستخدام اسم المستخدم أو البريد الإلكتروني أو وسيلة الاتصال.",
"pathToJellyfin": "مسار مجلد تكوين Jellyfin (config)",
"pathToJellyfinNotice": "إذا كنت لا تعرف مكان المسار، فحاول إعادة تعيين كلمة مرورك في Jellyfin. ستظهر نافذة منبثقة تحتوي على \"<path to jellyfin>/passwordreset-*.json\". هذا ليس ضروريًا إذا كنت ترغب فقط في استخدام خدمة إعادة تعيين كلمة المرور الذاتية من خلال \"صفحة المستخدم\".",
"resetLinks": "أرسل رابطاً بدلاً من رمز (PIN)",
"resetLinksNotice": "إذا تم إعداد Ombi، فاستخدم هذا لمزامنة إعادة تعيين كلمة مرور Jellyfin مع Ombi.",
"resetLinksLanguage": "لغة رابط إعادة التعيين الافتراضية",
"setPassword": "تعيين كلمة المرور من خلال رابط",
"setPasswordNotice": "تفعيل هذه الميزة يعني أن المستخدم لن يضطر إلى تغيير كلمة المرور من الرمز (PIN) بعد إعادة التعيين. سيتم أيضاً فرض متطلبات كلمة المرور.",
"moreInfo": "يمكنك العثور على مزيد من المعلومات حول الطرق المختلفة لإعادة تعيين كلمات المرور على {n}.",
"resetLinksRequiredForUserPage": "مطلوب للسماح بإعادة تعيين كلمة المرور ذاتياً على صفحة المستخدم."
},
"passwordValidation": {
"title": "",
"description": "",
"length": "",
"uppercase": "",
"lowercase": "",
"numbers": "",
"special": ""
"title": "متطلبات كلمة المرور",
"description": "عند التفعيل، ستظهر مجموعة من متطلبات كلمة المرور على صفحة إنشاء الحساب، مثل الحد الأدنى لطول الكلمة، وعدد الأحرف الكبيرة/الصغيرة، وما إلى ذلك.",
"length": "طول الكلمة",
"uppercase": "عدد الأحرف الكبيرة",
"lowercase": "عدد الأحرف الصغيرة",
"numbers": "عدد الأرقام",
"special": "عدد الحروف الخاصة (%, *, إلخ.)"
},
"helpMessages": {
"title": "",
"description": "",
"contactMessage": "",
"contactMessageNotice": "",
"helpMessage": "",
"helpMessageNotice": "",
"successMessage": "",
"successMessageNotice": "",
"emailMessage": "",
"emailMessageNotice": ""
"title": "رسائل المساعدة",
"description": "ستظهر هذه الرسائل في صفحة إنشاء الحساب وفي بعض رسائل البريد الإلكتروني.",
"contactMessage": "رسالة الاتصال",
"contactMessageNotice": "يتم عرضها في أسفل جميع الصفحات باستثناء صفحة المسؤول.",
"helpMessage": "رسالة المساعدة",
"helpMessageNotice": "يتم عرضها على صفحة إنشاء الحساب.",
"successMessage": "رسالة النجاح",
"successMessageNotice": "تظهر عندما يُتم المستخدم إنشاء حسابه.",
"emailMessage": "رسالة البريد الإلكتروني",
"emailMessageNotice": "تظهر في نهاية رسائل البريد الإلكتروني.",
"markdownMessageNotice": "يمكن تخصيص محتويات بعض رسائل البريد الإلكتروني والصفحات والرسائل باستخدام ترميز Markdown في الإعدادات."
},
"jellyseerr": {
"description": "Jellyseerr هو بديل لـ Ombi، ويتكامل مع jfa-go بشكل أفضل. بعد الانتهاء من الإعداد، انتقل إلى الإعدادات لإنشاء ملف تعريف وإضافة قالب لحسابات Jellyseerr الجديدة.",
"title": "إعداد Jellyseerr",
"importExisting": "استيراد المستخدمين الحاليين",
"importExistingDescription": "عند التفعيل، سيتم مزامنة تفاصيل وسائل الاتصال والتفضيلات الخاصة بالمستخدمين الحاليين من jfa-go."
},
"proxy": {
"title": "الوكيل",
"description": "دع jfa-go يقوم بجميع الاتصالات من خلال وكيل HTTP/SOCKS5. سيتم اختبار الاتصال بـ Jellyfin من خلاله.",
"protocol": "البروتوكول",
"address": "العنوان (متضمناً المنفذ)"
},
"userPage": {
"description": "تتيح صفحة المستخدم (التي تظهر باسم \"حسابي\") للمستخدمين الوصول إلى معلومات حسابهم، مثل وسائل الاتصال ومدة صلاحية الحساب. كما يمكنهم تغيير كلمة المرور، وطلب إعادة تعيينها ، وربط/تغيير وسائل الاتصال، دون الحاجة إلى طلب ذلك منك. بالإضافة إلى ذلك، يمكن عرض رسائل بترميز Markdown مخصصة للمستخدمين قبل وبعد تسجيل الدخول.",
"title": "صفحة المستخدم",
"customizeMessages": "اضغط زر التعديل بجوار \"صفحة المستخدم\" في الإعدادات لتعيينها لاحقاً.",
"requiredSettings": "يجب تفعيل تسجيل الدخول إلى jfa-go عبر Jellyfin. تأكد من اختيار \"إعادة تعيين كلمة المرور عبر رابط\" لاحقاً للسماح بإعادة تعيين كلمة المرور ذاتياً."
}
}

View File

@@ -3,14 +3,17 @@
"name": "العربية (AR)"
},
"strings": {
"startMessage": "مرحبا!\nقم بإدخال رمز Jellyfin للتحقق من حسابك.",
"discordStartMessage": "مرحباً!\nضع بإدخال الرمز بإستخدام `/pin <PIN>` للتحقق من حسابك.",
"matrixStartMessage": "مرحباً\nأدخل رقم التعريف الشخصي أدناه في صفحة الاشتراك في Jellyfin للتحقق من حسابك.",
"invalidPIN": "رقم التعريف الشخصي هذا غير صالح ، حاول مرة أخرى.",
"startMessage": "أهلاً!\nأدخل رمز Jellyfin هنا لتأكيد حسابك.",
"discordStartMessage": "أهلاً!\nأدخل رمزك باستخدام .لتأكيد حسابك /pin <الرمز>",
"matrixStartMessage": "أهلاً\nأدخل الرمز أدناه في صفحة التسجيل في Jellyfin لتأكيد حسابك.",
"invalidPIN": "هذا الرمز خاطئ، حاول مرة أُخرى.",
"pinSuccess": "تم! يمكنك الآن العودة إلى صفحة التسجيل.",
"languageMessage": "ملاحظة: اطّلع على اللغات المتاحة باستخدام {command} ، وقم بتعيين اللغة باستخدام {command} <language code>.",
"languageMessageDiscord": "ملاحظة: اضبط لغتك باستخدام / lang <language name>.",
"languageMessage": "ملاحظة: اطّلع على اللغات المتاحة باستخدام {command}، وقم بتعيين اللغة باستخدام {command} <language code>.",
"languageMessageDiscord": "ملاحظة: اختر اللغة باستخدام ./lang <language name>",
"languageSet": "تم تعيين اللغة إلى {language}.",
"discordDMs": "يرجى التحقق من الرسائل المباشرة الخاصة بك للحصول على رد."
"discordDMs": "يرجى التحقق من رسائلك المباشرة للحصول على رد.",
"noPermission": "لا تملك الصلاحية لهذا الإجراء.",
"sentInvite": "تم إرسال الدعوة.",
"sentInviteFailure": "فشل إرسال الدعوة، تحقق من السجلات."
}
}

View File

@@ -1,6 +1,6 @@
{
"meta": {
"name": "انگلیسی"
"name": "انگلیسی (FA)"
},
"strings": {
"startMessage": "سلام!\nبرای تأیید حساب خود ، کد PIN خود را در اینجا وارد کنید.",

View File

@@ -109,9 +109,11 @@ const (
GenerateInvite = "Generating new invite"
FailedGenerateInvite = "Failed to generate new invite: %v"
InvalidInviteCode = "Invalid invite code \"%s\""
FailedGetInvite = "Failed to get invite \"%s\": %v"
FailedSendToTooltipNoUser = "Failed: \"%s\" not found"
FailedSendToTooltipMultiUser = "Failed: \"%s\" linked to multiple users"
InvalidAddress = "invalid address \"%s\""
FailedParseTime = "Failed to parse time value: %v"
@@ -196,6 +198,8 @@ const (
UserAdminAdjusted = "Admin state for user \"%s\" set to %t"
UserLabelAdjusted = "Label for user \"%s\" set to \"%s\""
FailedGetJFActivities = "Failed to get ActivityLog entries: %v"
// api.go
ApplyUpdate = "Applied update"
FailedApplyUpdate = "Failed to apply update: %v"
@@ -278,7 +282,8 @@ const (
// NOTE: "migrations.go" is the one file where log messages are not part of logmessages/logmessages.go.
// pwreset.go
PWRExpired = "PWR for user \"%s\" already expired @ %s, check system time!"
PWRExpired = "PWR for user \"%s\" already expired @ %s, check system time!"
NewPWRForUser = "New password reset for user \"%s\""
// router.go
UseDefaultHTML = "Using default HTML \"%s\""
@@ -325,6 +330,12 @@ const (
// webhooks.go
WebhookRequest = "Webhook request send to \"%s\" (%d): %v"
// usercache.go
CacheRefreshCompleted = "Usercache refreshed, %d in %.2fs (%f.2u/sec)"
// Other
GotNEntries = "got %d entries"
)
const (

View File

@@ -8,7 +8,7 @@
Color-scheme: light dark;
supported-color-schemes: light dark;
}
body, .body {
.body {
background: #101010 !important;
background-color: #101010 !important;
}

69
main.go
View File

@@ -22,16 +22,17 @@ import (
"time"
"github.com/fatih/color"
"github.com/goccy/go-yaml"
"github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3"
"gopkg.in/ini.v1"
"github.com/hrfee/jfa-go/common"
_ "github.com/hrfee/jfa-go/docs"
"github.com/hrfee/jfa-go/jellyseerr"
"github.com/hrfee/jfa-go/logger"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/jfa-go/ombi"
"github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3"
"gopkg.in/ini.v1"
"gopkg.in/yaml.v3"
)
var (
@@ -77,7 +78,9 @@ var serverTypes = map[string]string{
"jellyfin": "Jellyfin",
"emby": "Emby (experimental)",
}
var serverType = mediabrowser.JellyfinServer
var substituteStrings = ""
var externalURI, externalDomain string // The latter lower-case as should be accessed through app.ExternalDomain()
@@ -112,19 +115,22 @@ type appContext struct {
adminUsers []User
invalidTokens []string
// Keeping jf name because I can't think of a better one
jf *mediabrowser.MediaBrowser
authJf *mediabrowser.MediaBrowser
ombi *OmbiWrapper
js *JellyseerrWrapper
thirdPartyServices []ThirdPartyService
storage *Storage
validator Validator
email *Emailer
telegram *TelegramDaemon
discord *DiscordDaemon
matrix *MatrixDaemon
housekeepingDaemon, userDaemon *GenericDaemon
contactMethods []ContactMethodLinker
jf struct {
*mediabrowser.MediaBrowser
activity *JFActivityCache
}
authJf *mediabrowser.MediaBrowser
ombi *OmbiWrapper
js *JellyseerrWrapper
thirdPartyServices []ThirdPartyService
storage *Storage
validator Validator
email *Emailer
telegram *TelegramDaemon
discord *DiscordDaemon
matrix *MatrixDaemon
housekeepingDaemon, userDaemon, jellyseerrDaemon *GenericDaemon
contactMethods []ContactMethodLinker
LoggerSet
host string
port int
@@ -164,7 +170,7 @@ func test(app *appContext) {
fmt.Println(n, ":", v)
}
users, err := app.jf.GetUsers(false)
fmt.Printf("GetUsers: err %s maplength %d\n", err, len(users))
fmt.Printf("GetUsers: err %+v maplength %d\n", err, len(users))
fmt.Printf("View output? [y/n]: ")
var choice string
fmt.Scanln(&choice)
@@ -176,7 +182,7 @@ func test(app *appContext) {
var username string
fmt.Scanln(&username)
user, err := app.jf.UserByName(username, false)
fmt.Printf("UserByName (%s): err %v", username, err)
fmt.Printf("UserByName (%s): err %+v", username, err)
out, _ := json.MarshalIndent(user, "", " ")
fmt.Print(string(out))
}
@@ -426,7 +432,7 @@ func start(asDaemon, firstCall bool) {
app.info.Println(lm.UsingJellyfin)
}
app.jf, err = mediabrowser.NewServer(
app.jf.MediaBrowser, err = mediabrowser.NewServer(
serverType,
server,
app.config.Section("jellyfin").Key("client").String(),
@@ -439,6 +445,10 @@ func start(asDaemon, firstCall bool) {
if err != nil {
app.err.Fatalf(lm.FailedAuthJellyfin, server, -1, err)
}
app.jf.activity = NewJFActivityCache(
app.jf,
time.Duration(app.config.Section("jellyfin").Key("activity_cache_sync_timeout_seconds").MustInt(20))*time.Second,
)
/*if debugMode {
app.jf.Verbose = true
}*/
@@ -516,12 +526,16 @@ func start(asDaemon, firstCall bool) {
go app.userDaemon.run()
defer app.userDaemon.Shutdown()
var jellyseerrDaemon *GenericDaemon
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) && app.config.Section("jellyseerr").Key("import_existing").MustBool(false) {
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
// import_existing_users setting is deprecated, now it'll run when jellyseerr is enabled, or when triggered manually.
// jellyseerrDaemon = newJellyseerrDaemon(time.Duration(30*time.Second), app)
jellyseerrDaemon = newJellyseerrDaemon(time.Duration(10*time.Minute), app)
go jellyseerrDaemon.run()
defer jellyseerrDaemon.Shutdown()
app.jellyseerrDaemon = newJellyseerrDaemon(time.Duration(24*time.Hour), app)
if app.jellyseerrDaemon != nil {
go app.jellyseerrDaemon.run()
// Run on startup
go app.jellyseerrDaemon.Trigger()
defer app.jellyseerrDaemon.Shutdown()
}
}
if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer {
@@ -711,7 +725,7 @@ func flagPassed(name string) (found bool) {
}
// @title jfa-go internal API
// @version 0.6.0
// @version 0.6.1
// @description API for the jfa-go frontend
// @contact.name Harvey Tindall
// @contact.email hrfee@hrfee.dev
@@ -762,6 +776,9 @@ func flagPassed(name string) (found bool) {
// @tag.name Statistics
// @tag.description Routes that expose useful info/stats.
// @tag.name Tasks
// @tag.description Manual triggers for background tasks.
func printVersion() {
tray := ""
if TRAY {

View File

@@ -8,7 +8,9 @@ import (
"github.com/hrfee/jfa-go/logger"
lm "github.com/hrfee/jfa-go/logmessages"
_ "github.com/mattn/go-sqlite3"
"maunium.net/go/mautrix/crypto/cryptohelper"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
@@ -35,7 +37,17 @@ func InitMatrixCrypto(d *MatrixDaemon, logger *logger.Logger) error {
dbPath := d.app.config.Section("files").Key("matrix_sql").String()
var err error
d.crypto = &Crypto{}
// bmss, err := NewBackedMemoryStateStore(d.app.storage.db)
// if err != nil {
// return err
// }
// d.bot.StateStore = bmss
d.crypto.helper, err = cryptohelper.NewCryptoHelper(d.bot, []byte("jfa-go"), dbPath)
// bms, err := NewBackedMemoryStore(d.app.storage.db)
// if err != nil {
// return err
// }
// d.crypto.helper, err = cryptohelper.NewCryptoHelper(d.bot, []byte("jfa-go"), bms)
if err != nil {
return err
}
@@ -63,3 +75,55 @@ func EncryptRoom(d *MatrixDaemon, roomID id.RoomID) error {
})
return err
}
/*type BackedMemoryStore struct {
*crypto.MemoryStore
db *badgerhold.Store
}
func (b *BackedMemoryStore) save() error {
err := b.db.Upsert("MatrixEncryptionStore", b.MemoryStore)
defer func(err error) { log.Printf("MATRIX WRITE: err=%v\n", err) }(err)
return err
}
func NewBackedMemoryStore(db *badgerhold.Store) (*BackedMemoryStore, error) {
b := &BackedMemoryStore{
db: db,
}
b.MemoryStore = crypto.NewMemoryStore(b.save)
err := b.db.Get("MatrixEncryptionStore", b.MemoryStore)
if err != nil && !errors.Is(err, badgerhold.ErrNotFound) {
return nil, err
}
return b, nil
}
type BackedMemoryStateStore struct {
*mautrix.MemoryStateStore
db *badgerhold.Store
}
func (b *BackedMemoryStateStore) save() error {
err := b.db.Upsert("MatrixEncryptionStateStore", b.MemoryStateStore)
defer func(err error) { log.Printf("MATRIX WRITE: err=%v\n", err) }(err)
return err
}
func NewBackedMemoryStateStore(db *badgerhold.Store) (*BackedMemoryStateStore, error) {
b := &BackedMemoryStateStore{
db: db,
}
store := mautrix.NewMemoryStateStore()
memStore, ok := store.(*mautrix.MemoryStateStore)
if !ok {
return nil, errors.New("didn't get a MemoryStateStore")
}
b.MemoryStateStore = memStore
err := b.db.Get("MatrixEncryptionStateStore", b.MemoryStateStore)
if err != nil && !errors.Is(err, badgerhold.ErrNotFound) {
return nil, err
}
return b, nil
}*/

View File

@@ -22,6 +22,8 @@ func runMigrations(app *appContext) {
// migrateHyphens(app)
migrateToBadger(app)
intialiseCustomContent(app)
migrateJellyseerrImportDaemon(app)
migratePWREmailPath(app)
}
// Migrate pre-0.2.0 user templates to profiles
@@ -187,7 +189,7 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
idList[user.JellyfinID] = vals
}
for jfID, ids := range idList {
ombiUser, err := app.getOmbiUser(jfID)
ombiUser, err := app.getOmbiUser(jfID, nil)
if err != nil {
app.debug.Printf("Failed to get Ombi user with Discord/Telegram \"%s\"/\"%s\": %v", ids[0], ids[1], err)
continue
@@ -505,3 +507,31 @@ func migrateExternalURL(app *appContext) {
return
}
}
// Migrate from use of "Import Existing Users" Jellyseerr import daemon to one-time run when enabling the feature.
func migrateJellyseerrImportDaemon(app *appContext) {
// When Jellyseerr is disabled, set this flag to false so that an initial sync happens the next time it is enabled.
if !(app.config.Section("jellyseerr").Key("enabled").MustBool(false)) {
app.storage.db.Upsert("jellyseerr_inital_sync_status", JellyseerrInitialSyncStatus{false})
}
}
// Migrate previous filepath to the password reset email (email.html/txt) to new one.
func migratePWREmailPath(app *appContext) {
if app.config.Section("password_resets").Key("email_html").String() != "jfa-go:email.html" && app.config.Section("password_resets").Key("email_text").String() != "jfa-go:email.txt" {
return
}
tempConfig, _ := ini.ShadowLoad(app.configPath)
if app.config.Section("password_resets").Key("email_html").String() == "jfa-go:email.html" {
tempConfig.Section("password_resets").Key("email_html").SetValue("")
}
if app.config.Section("password_resets").Key("email_text").String() == "jfa-go:email.txt" {
tempConfig.Section("password_resets").Key("email_text").SetValue("")
}
err := tempConfig.SaveTo(app.configPath)
if err != nil {
app.err.Fatalf("Failed to save config: %v", err)
return
}
app.ReloadConfig()
}

126
models.go
View File

@@ -2,6 +2,8 @@ package main
import (
"time"
"github.com/hrfee/mediabrowser"
)
type stringResponse struct {
@@ -52,16 +54,16 @@ type enableDisableUserDTO struct {
}
type generateInviteDTO struct {
Months int `json:"months" example:"0"` // Number of months
Days int `json:"days" example:"1"` // Number of days
Hours int `json:"hours" example:"2"` // Number of hours
Minutes int `json:"minutes" example:"3"` // Number of minutes
UserExpiry bool `json:"user-expiry"` // Whether or not user expiry is enabled
UserMonths int `json:"user-months,omitempty" example:"1"` // Number of months till user expiry
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
SendTo string `json:"send-to" example:"jeff@jellyf.in"` // Send invite to this address or discord name
Months int `json:"months" example:"0"` // Number of months
Days int `json:"days" example:"1"` // Number of days
Hours int `json:"hours" example:"2"` // Number of hours
Minutes int `json:"minutes" example:"3"` // Number of minutes
UserExpiry bool `json:"user-expiry"` // Whether or not user expiry is enabled
UserMonths int `json:"user-months,omitempty" example:"1"` // Number of months till user expiry
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
sendInviteDTO
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
@@ -70,9 +72,13 @@ type generateInviteDTO struct {
UserLabel string `json:"user_label,omitempty" example:"Friend"` // Label to apply to users created w/ this invite.
}
type inviteProfileDTO struct {
Invite string `json:"invite" example:"slakdaslkdl2342"` // Invite to apply to
Profile string `json:"profile" example:"DefaultProfile"` // Profile to use
type SendInviteDTO struct {
Invite string `json:"invite" example:"slakdaslkdl2342"` // Invite to apply to
sendInviteDTO
}
type sendInviteDTO struct {
SendTo string `json:"send-to" example:"jeff@jellyf.in"` // Send invite to this address or discord name
}
type profileDTO struct {
@@ -102,29 +108,33 @@ type newProfileDTO struct {
ID string `json:"id" example:"ZXhhbXBsZTEyMzQ1Njc4OQo" binding:"required"` // ID of user to source settings from
Homescreen bool `json:"homescreen" example:"true"` // Whether to store homescreen layout or not
OmbiID string `json:"ombi_id" example:"ZXhhbXBsZTEyMzQ1Njc4OQo"` // ID of Ombi user to source settings from (optional)
Jellyseerr bool `json:"jellyseerr"` // Whether or not to generate Jellyseerr profile from user
}
type inviteDTO struct {
Code string `json:"code" example:"sajdlj23423j23"` // Invite code
Months int `json:"months" example:"1"` // Number of months till expiry
Days int `json:"days" example:"1"` // Number of days till expiry
Hours int `json:"hours" example:"2"` // Number of hours till expiry
Minutes int `json:"minutes" example:"3"` // Number of minutes till expiry
UserExpiry bool `json:"user-expiry"` // Whether or not user expiry is enabled
UserMonths int `json:"user-months,omitempty" example:"1"` // Number of months till user expiry
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
Created int64 `json:"created" example:"1617737207510"` // Date of creation
Profile string `json:"profile" example:"DefaultProfile"` // Profile used on this invite
UsedBy map[string]int64 `json:"used-by,omitempty"` // Users who have used this invite mapped to their creation time in Epoch/Unix time
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
SendTo string `json:"send_to,omitempty"` // Email/Discord username the invite was sent to (if applicable)
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
UserLabel string `json:"user_label,omitempty" example:"Friend"` // Label to apply to users created w/ this invite.
EditableInviteDTO
ValidTill int64 `json:"valid_till" example:"1617737207510"` // Unix timestamp of expiry
Created int64 `json:"created" example:"1617737207510"` // Date of creation
UsedBy map[string]int64 `json:"used_by,omitempty"` // Users who have used this invite mapped to their creation time in Epoch/Unix time
NoLimit bool `json:"no_limit"` // If true, invite can be used any number of times
RemainingUses int `json:"remaining_uses,omitempty"` // Remaining number of uses (if applicable)
SendTo string `json:"send_to,omitempty"` // DEPRECATED Email/Discord username the invite was sent to (if applicable)
SentTo SentToList `json:"sent_to,omitempty"` // Email/Discord usernames attempts were made to send this invite to, and a failure reason if failed.
}
type EditableInviteDTO struct {
Code string `json:"code" example:"sajdlj23423j23"` // Invite code
NotifyExpiry *bool `json:"notify_expiry,omitempty"` // Whether to notify the requesting user of expiry or not
NotifyCreation *bool `json:"notify_creation,omitempty"` // Whether to notify the requesting user of account creation or not
Label *string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
UserLabel *string `json:"user_label,omitempty" example:"Friend"` // Label to apply to users created w/ this invite.
Profile *string `json:"profile" example:"DefaultProfile"` // Profile used on this invite
UserExpiry *bool `json:"user_expiry"` // Whether or not user expiry is enabled
UserMonths *int `json:"user_months,omitempty" example:"1"` // Number of months till user expiry
UserDays *int `json:"user_days,omitempty" example:"1"` // Number of days till user expiry
UserHours *int `json:"user_hours,omitempty" example:"2"` // Number of hours till user expiry
UserMinutes *int `json:"user_minutes,omitempty" example:"3"` // Number of minutes till user expiry
}
type getInvitesDTO struct {
@@ -164,15 +174,31 @@ type respUser struct {
ReferralsEnabled bool `json:"referrals_enabled"`
}
// ServerSearchReqDTO is a usual SortablePaginatedReqDTO with added fields for searching and filtering.
type ServerSearchReqDTO struct {
SortablePaginatedReqDTO
ServerFilterReqDTO
}
// ServerFilterReqDTO provides search terms and queries to a search or count route.
type ServerFilterReqDTO struct {
SearchTerms []string `json:"searchTerms"`
Queries []QueryDTO `json:"queries"`
}
type PaginatedDTO struct {
LastPage bool `json:"last_page"`
}
type PaginatedReqDTO struct {
Limit int `json:"limit"`
Page int `json:"page"` // zero-indexed
type SortablePaginatedReqDTO struct {
SortByField string `json:"sortByField"`
Ascending bool `json:"ascending"`
PaginatedReqDTO
}
type PaginatedReqDTO struct {
Limit int `json:"limit"`
Page int `json:"page"` // zero-indexed
}
type getUsersDTO struct {
@@ -491,3 +517,31 @@ type PagePathsDTO struct {
// The subdirectory the app is meant to be accessed from ("Reverse proxy subfolder")
TrueBase string `json:"TrueBase"`
}
type TasksDTO struct {
Tasks []TaskDTO `json:"tasks"`
}
type TaskDTO struct {
URL string `json:"url"`
Name string `json:"name"`
Description string `json:"description"`
}
type LabelsDTO struct {
Labels []string `json:'labels"`
}
type ActivityLogEntriesDTO struct {
Entries []ActivityLogEntryDTO `json:"entries"`
}
type PaginatedActivityLogEntriesDTO struct {
ActivityLogEntriesDTO
PaginatedDTO
}
type ActivityLogEntryDTO struct {
mediabrowser.ActivityLogEntry
Date int64 `json:"Date"`
}

398
package-lock.json generated
View File

@@ -10,13 +10,16 @@
"license": "ISC",
"dependencies": {
"@af-utils/scrollend-polyfill": "^0.0.14",
"@hrfee/simpletemplate": "^1.1.0",
"@ts-stack/markdown": "^1.4.0",
"@types/node": "^20.3.0",
"@webcoder49/code-input": "^2.7.2",
"a17t": "^0.10.1",
"any-date-parser": "^1.5.4",
"browserslist": "^4.21.7",
"cheerio": "^1.0.0-rc.12",
"fs-cheerio": "^3.0.0",
"highlight.js": "^11.11.1",
"inline-source": "^8.0.2",
"jsdom": "^22.1.0",
"lodash": "^4.17.21",
@@ -27,14 +30,15 @@
"remixicon": "^4.3.0",
"remove-markdown": "^0.5.0",
"tailwindcss": "^3.3.2",
"typescript": "^5.1.3",
"temporal-polyfill": "^0.3.0",
"uncss": "^0.17.3"
},
"devDependencies": {
"@typescript/native-preview": "^7.0.0-dev.20251209.1",
"live-server": "^1.2.2"
},
"optionalDependencies": {
"esbuild": "^0.25.0"
"esbuild": "^0.25.12"
}
},
"node_modules/@af-utils/scrollend-polyfill": {
@@ -65,9 +69,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
"integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
@@ -81,9 +85,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
"integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
@@ -97,9 +101,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
"integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
@@ -113,9 +117,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
"integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
@@ -129,9 +133,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
"integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
@@ -145,9 +149,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
"integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
@@ -161,9 +165,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
"integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
@@ -177,9 +181,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
"integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
@@ -193,9 +197,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
"integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
@@ -209,9 +213,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
"integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
@@ -225,9 +229,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
"integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
@@ -241,9 +245,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
"integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
@@ -257,9 +261,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
"integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
@@ -273,9 +277,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
"integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
@@ -289,9 +293,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
"integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
@@ -305,9 +309,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
"integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
@@ -321,9 +325,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
"integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
@@ -337,9 +341,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
"integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
@@ -353,9 +357,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
"integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
@@ -369,9 +373,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
"integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
@@ -385,9 +389,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
"integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
@@ -401,9 +405,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
"integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
@@ -417,9 +421,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
"integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
@@ -433,9 +437,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
"integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
@@ -449,9 +453,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
"integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
@@ -465,9 +469,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
"integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
@@ -480,6 +484,12 @@
"node": ">=18"
}
},
"node_modules/@hrfee/simpletemplate": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@hrfee/simpletemplate/-/simpletemplate-1.1.0.tgz",
"integrity": "sha512-5H4/y7CegE4twstRPip/ms+OGUb+BPyVt+hriEpY88lTWW4I6jxzHFHmz6NDZmVyRa4RVIEVBxvtwk3RXKJVwA==",
"license": "MIT"
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -629,6 +639,129 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@typescript/native-preview": {
"version": "7.0.0-dev.20251209.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20251209.1.tgz",
"integrity": "sha512-xnx3A1S1TTx+mx8FfP1UwkNTwPBmhGCbOh4PDNRUV5gDZkVuDDN3y1F7NPGSMg6MXE1KKPSLNM+PQMN33ZAL2Q==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsgo": "bin/tsgo.js"
},
"optionalDependencies": {
"@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251209.1",
"@typescript/native-preview-darwin-x64": "7.0.0-dev.20251209.1",
"@typescript/native-preview-linux-arm": "7.0.0-dev.20251209.1",
"@typescript/native-preview-linux-arm64": "7.0.0-dev.20251209.1",
"@typescript/native-preview-linux-x64": "7.0.0-dev.20251209.1",
"@typescript/native-preview-win32-arm64": "7.0.0-dev.20251209.1",
"@typescript/native-preview-win32-x64": "7.0.0-dev.20251209.1"
}
},
"node_modules/@typescript/native-preview-darwin-arm64": {
"version": "7.0.0-dev.20251209.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20251209.1.tgz",
"integrity": "sha512-F1cnYi+ZeinYQnaTQKKIsbuoq8vip5iepBkSZXlB8PjbG62LW1edUdktd/nVEc+Q+SEysSQ3jRdk9eU766s5iw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@typescript/native-preview-darwin-x64": {
"version": "7.0.0-dev.20251209.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20251209.1.tgz",
"integrity": "sha512-Ta6XKdAxEMBzd1xS4eQKXmlUkml+kMf23A9qFoegOxmyCdHJPak2gLH9ON5/C6js0ibZm1kdqwbcA0/INrcThg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@typescript/native-preview-linux-arm": {
"version": "7.0.0-dev.20251209.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20251209.1.tgz",
"integrity": "sha512-4e7WSBLLdmfJUGzm9Id4WA2fDZ2sY3Q6iudyZPNSb5AFsCmqQksM/JGAlNROHpi/tIqo95e3ckbjmrZTmH60EA==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@typescript/native-preview-linux-arm64": {
"version": "7.0.0-dev.20251209.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20251209.1.tgz",
"integrity": "sha512-kdiPMvs1hwi76hgvZjz4XQVNYTV+MAbJKnHXz6eL6aVXoTYzNtan5vWywKOHv9rV4jBMyVlZqtKbeG/XVV9WdQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@typescript/native-preview-linux-x64": {
"version": "7.0.0-dev.20251209.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20251209.1.tgz",
"integrity": "sha512-dH/Z50Xb52N4Csd0BXptmjuMN+87AhUAjM9Y5rNU8VwcUJJDFpKM6aKUhd4Q+XEVJWPFPlKDLx3pVhnO31CBhQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@typescript/native-preview-win32-arm64": {
"version": "7.0.0-dev.20251209.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20251209.1.tgz",
"integrity": "sha512-vW7IGRNIUhhQ0vzFY3sRNxvYavNGum2OWgW1Bwc05yhg9AexBlRjdhsUSTLQ2dUeaDm2nx4i38LhXIVgLzMNeA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@typescript/native-preview-win32-x64": {
"version": "7.0.0-dev.20251209.1",
"resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20251209.1.tgz",
"integrity": "sha512-jKT6npBrhRX/84LWSy9PbOWx2USTZhq9SOkvH2mcnU/+uqyNxZIMMVnW5exIyzcnWSPly3jK2qpfiHNjdrDaAA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@webcoder49/code-input": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/@webcoder49/code-input/-/code-input-2.7.2.tgz",
"integrity": "sha512-rW+bDCDhXJuUUS1+NMazYfHuaxgClcayEPbTXWsitqb0gRpyBaY2T83RM//YhRHzHPUYL4bX8hWU+zkj2u4LiA==",
"license": "MIT"
},
"node_modules/a17t": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/a17t/-/a17t-0.10.1.tgz",
@@ -1125,6 +1258,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@@ -2069,9 +2203,9 @@
}
},
"node_modules/esbuild": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -2082,32 +2216,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.8",
"@esbuild/android-arm": "0.25.8",
"@esbuild/android-arm64": "0.25.8",
"@esbuild/android-x64": "0.25.8",
"@esbuild/darwin-arm64": "0.25.8",
"@esbuild/darwin-x64": "0.25.8",
"@esbuild/freebsd-arm64": "0.25.8",
"@esbuild/freebsd-x64": "0.25.8",
"@esbuild/linux-arm": "0.25.8",
"@esbuild/linux-arm64": "0.25.8",
"@esbuild/linux-ia32": "0.25.8",
"@esbuild/linux-loong64": "0.25.8",
"@esbuild/linux-mips64el": "0.25.8",
"@esbuild/linux-ppc64": "0.25.8",
"@esbuild/linux-riscv64": "0.25.8",
"@esbuild/linux-s390x": "0.25.8",
"@esbuild/linux-x64": "0.25.8",
"@esbuild/netbsd-arm64": "0.25.8",
"@esbuild/netbsd-x64": "0.25.8",
"@esbuild/openbsd-arm64": "0.25.8",
"@esbuild/openbsd-x64": "0.25.8",
"@esbuild/openharmony-arm64": "0.25.8",
"@esbuild/sunos-x64": "0.25.8",
"@esbuild/win32-arm64": "0.25.8",
"@esbuild/win32-ia32": "0.25.8",
"@esbuild/win32-x64": "0.25.8"
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/escalade": {
@@ -2928,6 +3062,15 @@
"he": "bin/he"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
@@ -4590,9 +4733,9 @@
}
},
"node_modules/nan": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz",
"integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==",
"dev": true,
"license": "MIT",
"optional": true
@@ -5180,6 +5323,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -5536,6 +5680,7 @@
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
@@ -6526,6 +6671,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -6727,6 +6873,21 @@
"node": ">=8.0"
}
},
"node_modules/temporal-polyfill": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/temporal-polyfill/-/temporal-polyfill-0.3.0.tgz",
"integrity": "sha512-qNsTkX9K8hi+FHDfHmf22e/OGuXmfBm9RqNismxBrnSmZVJKegQ+HYYXT+R7Ha8F/YSm2Y34vmzD4cxMu2u95g==",
"license": "MIT",
"dependencies": {
"temporal-spec": "0.3.0"
}
},
"node_modules/temporal-spec": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/temporal-spec/-/temporal-spec-0.3.0.tgz",
"integrity": "sha512-n+noVpIqz4hYgFSMOSiINNOUOMFtV5cZQNCmmszA6GiVFVRt3G7AqVyhXjhCSmowvQn+NsGn+jMDMKJYHd3bSQ==",
"license": "ISC"
},
"node_modules/terser": {
"version": "5.43.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
@@ -6922,19 +7083,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",

View File

@@ -18,13 +18,16 @@
"homepage": "https://github.com/hrfee/jfa-go#readme",
"dependencies": {
"@af-utils/scrollend-polyfill": "^0.0.14",
"@hrfee/simpletemplate": "^1.1.0",
"@ts-stack/markdown": "^1.4.0",
"@types/node": "^20.3.0",
"@webcoder49/code-input": "^2.7.2",
"a17t": "^0.10.1",
"any-date-parser": "^1.5.4",
"browserslist": "^4.21.7",
"cheerio": "^1.0.0-rc.12",
"fs-cheerio": "^3.0.0",
"highlight.js": "^11.11.1",
"inline-source": "^8.0.2",
"jsdom": "^22.1.0",
"lodash": "^4.17.21",
@@ -35,13 +38,14 @@
"remixicon": "^4.3.0",
"remove-markdown": "^0.5.0",
"tailwindcss": "^3.3.2",
"typescript": "^5.1.3",
"temporal-polyfill": "^0.3.0",
"uncss": "^0.17.3"
},
"devDependencies": {
"@typescript/native-preview": "^7.0.0-dev.20251209.1",
"live-server": "^1.2.2"
},
"optionalDependencies": {
"esbuild": "^0.25.0"
"esbuild": "^0.25.12"
}
}

View File

@@ -73,7 +73,7 @@ type PasswordReset struct {
}
func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
if !emailEnabled {
if !messagesEnabled {
return
}
for {
@@ -94,7 +94,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
app.debug.Printf(lm.FailedReading, event.Name, err)
continue
}
app.info.Printf("New password reset for user \"%s\"", pwr.Username)
app.info.Printf(lm.NewPWRForUser, pwr.Username)
if currentTime := time.Now(); pwr.Expiry.After(currentTime) {
user, err := app.jf.UserByName(pwr.Username, false)
if err != nil || user.ID == "" {

View File

@@ -202,22 +202,29 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/users/count", app.GetUserCount)
api.POST(p+"/users", app.SearchUsers)
api.POST(p+"/users/count", app.GetFilteredUserCount)
api.GET(p+"/users/labels", app.GetLabels)
api.POST(p+"/user", app.NewUserFromAdmin)
api.POST(p+"/users/extend", app.ExtendExpiry)
api.DELETE(p+"/users/:id/expiry", app.RemoveExpiry)
api.GET(p+"/users/:id", app.GetUser)
api.GET(p+"/users/:id/activities/jellyfin", app.GetJFActivitesForUser)
api.GET(p+"/users/:id/activities/jellyfin/count", app.CountJFActivitesForUser)
api.POST(p+"/users/:id/activities/jellyfin", app.GetPaginatedJFActivitesForUser)
api.POST(p+"/users/enable", app.EnableDisableUsers)
api.POST(p+"/invites", app.GenerateInvite)
api.GET(p+"/invites", app.GetInvites)
api.GET(p+"/invites/count", app.GetInviteCount)
api.GET(p+"/invites/count/used", app.GetInviteUsedCount)
api.DELETE(p+"/invites", app.DeleteInvite)
api.POST(p+"/invites/profile", app.SetProfile)
api.POST(p+"/invites/send", app.SendInvite)
api.PATCH(p+"/invites/edit", app.EditInvite)
api.GET(p+"/profiles", app.GetProfiles)
api.GET(p+"/profiles/names", app.GetProfileNames)
api.GET(p+"/profiles/raw/:name", app.GetRawProfile)
api.PUT(p+"/profiles/raw/:name", app.ReplaceRawProfile)
api.POST(p+"/profiles/default", app.SetDefaultProfile)
api.POST(p+"/profiles", app.CreateProfile)
api.DELETE(p+"/profiles", app.DeleteProfile)
api.POST(p+"/invites/notify", app.SetNotify)
api.POST(p+"/users/emails", app.ModifyEmails)
api.POST(p+"/users/labels", app.ModifyLabels)
api.POST(p+"/users/accounts-admin", app.SetAccountsAdmin)
@@ -242,8 +249,12 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart)
api.GET(p+"/logs", app.GetLog)
api.POST(p+"/tasks/housekeeping", func(gc *gin.Context) { app.housekeepingDaemon.Trigger(); gc.Status(http.StatusNoContent) })
api.POST(p+"/tasks/users", func(gc *gin.Context) { app.userDaemon.Trigger(); gc.Status(http.StatusNoContent) })
api.GET(p+"/tasks", app.TaskList)
api.POST(p+"/tasks/housekeeping", app.TaskHousekeeping)
api.POST(p+"/tasks/users", app.TaskUserCleanup)
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
api.POST(p+"/tasks/jellyseerr", app.TaskJellyseerrImport)
}
api.POST(p+"/backups", app.CreateBackup)
api.GET(p+"/backups/:fname", app.GetBackup)
api.GET(p+"/backups", app.GetBackups)

View File

@@ -1,50 +0,0 @@
#!/usr/bin/env bash
# scan all typescript and automatically add dark variants to color tags if they're not already present.
for f in $1/*.ts; do
# FIXME: inline html
for l in $(grep -n "~neutral\|~positive\|~urge\|~warning\|~info\|~critical" $f | sed -e 's/:.*//g'); do
# for l in $(sed -n '/classList/=' $f); do
line=$(sed -n "${l}p" $f)
echo $line | grep "classList" &> /dev/null
if [ $? -eq 0 ]; then
echo $line | sed 's/.*classList//; s/).*//' | grep "~neutral\|~positive\|~urge\|~warning\|~info\|~critical" &> /dev/null
if [ $? -eq 0 ]; then
# echo "found classList @ " $l
echo $line | grep "dark:" &>/dev/null
if [ $? -ne 0 ]; then
for color in neutral positive urge warning info critical; do
sed -i "${l},${l}s/\"~${color}\"/\"~${color}\", \"dark:~d_${color}\"/g" $f
done
fi
else
echo "FIX: classList found, but color tag wasn't in it"
fi
else
echo $line | grep "querySelector" &> /dev/null
if [ $? -ne 0 ]; then
# echo "found inline in " $f " @ " $l ", " $(sed -n "${l}p" $f)
echo $line | grep "dark:" &>/dev/null
if [ $? -ne 0 ]; then
for color in neutral positive urge warning info critical; do
sed -i "${l},${l}s/~${color}/~${color} dark:~d_${color}/g" $f
done
fi
else
echo $line | sed 's/.*querySelector//; s/).*//' | grep "~neutral\|~positive\|~urge\|~warning\|~info\|~critical" &> /dev/null
if [ $? -ne 0 ]; then
echo $line | grep "dark:" &>/dev/null
if [ $? -ne 0 ]; then
# echo "found inline in " $f " @ " $l ", " $(sed -n "${l}p" $f)
for color in neutral positive urge warning info critical; do
sed -i "${l},${l}s/~${color}/~${color} dark:~d_${color}/g" $f
done
fi
#else
#echo "FIX: querySelector found, but color tag wasn't in it: " $line
fi
fi
fi
done
done

Some files were not shown because too many files have changed in this diff Show More