Compare commits

...

149 Commits

Author SHA1 Message Date
ClankJake
a0af76364a Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (7 of 7 strings)

Translation: jfa-go/Password Reset Links
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/password-reset-links/pt_BR/
2021-06-20 19:02:08 +02:00
ClankJake
169622bf95 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (6 of 6 strings)

Translation: jfa-go/Telegram/Matrix/Discord bots
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/chat-bots/pt_BR/
2021-06-20 19:02:08 +02:00
ClankJake
78b5136b9a Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (51 of 51 strings)

Translation: jfa-go/Emails
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/emails/pt_BR/
2021-06-20 19:02:08 +02:00
ClankJake
e546f50141 translation from Weblate (Portuguese (Brazil))
Currently translated at 99.3% (162 of 163 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/admin/pt_BR/
2021-06-20 19:02:08 +02:00
ClankJake
e35fe8d425 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (20 of 20 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/common-strings/pt_BR/
2021-06-20 19:02:08 +02:00
ClankJake
6ed2f7aaa6 translation from Weblate (Portuguese (Brazil))
Currently translated at 100.0% (37 of 37 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/form/pt_BR/
2021-06-20 19:02:08 +02:00
ClankJake
084e8ec432 Added translation using Weblate (Portuguese (Brazil)) 2021-06-20 19:02:08 +02:00
ClankJake
fd7f74682b Added translation using Weblate (Portuguese (Brazil)) 2021-06-20 19:02:08 +02:00
Harvey Tindall
9950c158a1 move copy button to right, don't uncss when DEBUG=on
keep unused css when DEBUG=on by skipping the uncss step.
2021-06-13 16:31:40 +01:00
Harvey Tindall
21125033ff Add copy button to crash page 2021-06-13 15:36:36 +01:00
Harvey Tindall
1dc0b2234a switch to normal npm mirror in package-lock
was using cnpm as i'd added it to help a user who was having problems
(ended up being internet-related, not blocking).
2021-06-11 23:46:01 +01:00
Harvey Tindall
0ea5c7fdc0 remove inline-css-cli build dep
unnecessary (inline-source-cli already includes its functionality) and a
dependency of it had a high-severity CVE (wouldn't have affected anyone,
but w/e).
2021-06-11 23:39:38 +01:00
Harvey Tindall
b538922c05 Show log on log.Fatal calls, provide "sanitized" version, fix goreleaser
Sanitization means change anything in double quotes to "CENSORED". A
notice is included telling the user to check for themselves as well.
2021-06-11 23:28:21 +01:00
Harvey Tindall
f0f4e8118e Generate crash report txt and webpage
The last 100 lines of logs are now cached, and when a crash occurs, they
are saved to a file in the temp directory ("/tmp" on *nix), and pretty
HTML version is also created and opened in the browser.
* Currently only handles panics, will be included in more places soon
* Copy button and button to generate a GH issue will be added
2021-06-11 21:56:53 +01:00
Harvey Tindall
2f501697db merge translations 2021-06-07 13:58:53 +01:00
Harvey Tindall
0a71d5b216 PWR: Add option to set new password from magic link
For #103. Enable in Settings > Password Resets. Also changes the user's
ombi password.
2021-06-07 13:49:05 +01:00
Harvey Tindall
0014db44f0 form: module-ize password validator 2021-06-05 22:17:10 +01:00
Harvey Tindall
885d2ebf0f admin: fix telegram/discord/matrix enabled varnames 2021-06-05 22:16:57 +01:00
Richard de Boer
6d089a9818 Translated using Weblate (Dutch)
Currently translated at 100.0% (6 of 6 strings)

Translation: jfa-go/Telegram/Matrix/Discord bots
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/chat-bots/nl/
2021-06-02 14:39:37 +02:00
Richard de Boer
de81c7e29f Added translation using Weblate (Dutch) 2021-06-02 14:39:37 +02:00
Harvey Tindall
e49996c401 Setup: Don't break if setup not completed, fix restart
Since an invalid example config was created on first run, if the app restarted
before setup was completed, it would crash on the next start. The
example now has a "first_run" flag in it, which is only set to false
when the config is modified. Also fixed restart at the end of setup for
tray builds.
2021-06-01 19:54:13 +01:00
Malte
aa40a72075 Translated using Weblate (German)
Currently translated at 100.0% (6 of 6 strings)

Translation: jfa-go/Telegram/Matrix/Discord bots
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/chat-bots/de/
2021-06-01 18:29:10 +02:00
Richard de Boer
19b7341e80 Translated using Weblate (Dutch)
Currently translated at 100.0% (20 of 20 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/common-strings/nl/
2021-06-01 18:29:10 +02:00
Malte
73645a7569 Translated using Weblate (German)
Currently translated at 100.0% (20 of 20 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/common-strings/de/
2021-06-01 18:29:10 +02:00
Malte
a9dac8c04c translation from Weblate (German)
Currently translated at 100.0% (37 of 37 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/form/de/
2021-06-01 18:29:09 +02:00
Malte
43fbbbcd16 translation from Weblate (German)
Currently translated at 100.0% (163 of 163 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/admin/de/
2021-06-01 18:29:09 +02:00
Richard de Boer
fc57a8c97f translation from Weblate (Dutch)
Currently translated at 100.0% (37 of 37 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/form/nl/
2021-06-01 18:29:07 +02:00
Richard de Boer
1fb2ef5675 translation from Weblate (Dutch)
Currently translated at 100.0% (163 of 163 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/admin/nl/
2021-06-01 18:29:06 +02:00
Malte
e0d994c35c Added translation using Weblate (German) 2021-06-01 17:21:53 +02:00
hrfee
cab30eb628 Added translation using Weblate (English (United Kingdom)) 2021-06-01 17:13:56 +02:00
Harvey Tindall
71df011556 merge translations 2021-06-01 16:13:28 +01:00
Levi
b2828110e3 Translated using Weblate (Swedish)
Currently translated at 100.0% (101 of 101 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/setup/sv/
2021-06-01 17:07:49 +02:00
Levi
50eb05776a translation from Weblate (Swedish)
Currently translated at 100.0% (32 of 32 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/form/sv/
2021-06-01 17:07:49 +02:00
Harvey Tindall
19715f25f6 Move all migrations to separate file
Also fixed some inconsistent uses of snake case.
2021-06-01 14:18:49 +01:00
Harvey Tindall
d41a281d53 mention tray icon in issue template 2021-06-01 00:54:22 +01:00
Harvey Tindall
a8229631bd mention matrix in readme 2021-06-01 00:06:35 +01:00
Harvey Tindall
0a2cf6132f merge matrix into local changes 2021-05-31 22:13:52 +01:00
Harvey Tindall
d7ab01063a apt: Use commit count to fix version comparisons
previous fix only worked because the numerical portion of the commit
hash happened to be greater. This should do properly fix it.
2021-05-31 22:11:43 +01:00
Harvey Tindall
6fb8f1ed7f Merge pull request #112 from hrfee/matrix
Matrix Integration
2021-05-31 21:12:57 +01:00
Harvey Tindall
a9b11012bc Matrix: Add example images 2021-05-31 20:56:29 +01:00
Harvey Tindall
e7cb1f516b Mention wiki in Telegram/Discord/Matrix settings descriptions 2021-05-31 20:32:16 +01:00
Harvey Tindall
555d5abf59 bump browserslist version 2021-05-31 18:53:21 +01:00
Harvey Tindall
93937ec3b5 drone: use pre-made image with dependencies
a cron job builds hrfee/jfa-go-build-docker daily, which includes the
apt dependencies for building.
2021-05-31 14:58:20 +01:00
Harvey Tindall
93c7a6e31b apt: Change versioning to hopefully fix updates
0.0.0-{commit} didn't register as updates to the previously installed
  versions, but it looks like 0{commit} works.
2021-05-31 14:13:41 +01:00
Harvey Tindall
a9cabe3d74 apt: also remove jfa-go-tray 2021-05-30 23:37:17 +01:00
Harvey Tindall
d6fd1d6894 apt: remove last unstable before adding
kind of a hack, since all versions have version number 0.0.0, reprepro
skips adding new files.
2021-05-30 23:26:50 +01:00
Harvey Tindall
375022ba95 Matrix: Add token generation wizard
Pressing the "+" next to matrix in settings allows you to enter a
homeserver, username and password to enable matrix and generate an
access token.
2021-05-30 23:09:20 +01:00
Harvey Tindall
75fdf6ec3d Matrix: Connect on accounts tab, customizable chat topic 2021-05-30 11:47:41 +01:00
Harvey Tindall
561c461a18 PWR: TrimPrefix instead of Replace for PWR links 2021-05-30 00:07:18 +01:00
Harvey Tindall
59ebf52fe2 Matrix: Show matrix on accounts page 2021-05-30 00:05:46 +01:00
Harvey Tindall
89fb3fa619 Matrix: Notifications 2021-05-29 21:05:12 +01:00
Harvey Tindall
9bd6abadf4 Matrix: Fix user storage 2021-05-29 19:50:33 +01:00
Harvey Tindall
953a66ec47 Password Resets: Ignore magic link visits from bots
For #108. Literally just searches the useragent for "Bot", seems good
enough for Telegram atleast.
2021-05-29 19:24:00 +01:00
Harvey Tindall
4e826f4167 Matrix: Store user on sign-up 2021-05-29 18:51:43 +01:00
Harvey Tindall
e97b90d4d7 Matrix: Setup bot, add PIN verification
PIN is verified but not used currently. Works a little different than
the others, you input your matrix user ID and then the PIN is sent to
you. The bot doesn't support E2EE, so the bot being the first one to
message ensures the chat is unencrypted.
2021-05-29 17:43:11 +01:00
Harvey Tindall
fb6256d1ed Telegram: Escape all necessary characters
Fixes #108.
2021-05-25 23:03:13 +01:00
Harvey Tindall
7035a3fe9c Tray: Add button to open logs 2021-05-25 20:16:42 +01:00
Harvey Tindall
62c29d55cc Log output to TEMP/jfa-go.log when Tray enabled
-H=windowsgui completely disables Stdout/Stderr on Windows, so logging
is enabled.
2021-05-25 17:59:41 +01:00
Harvey Tindall
a83dbcf3ab debian/ubuntu, not just debian 2021-05-25 01:33:58 +01:00
Harvey Tindall
e48bdcc45b README: change install section layout
Downloads at the top also now link to parts in the install section.
2021-05-24 22:58:11 +01:00
Harvey Tindall
0b473ef01f don't put .debs on buildrone; link to instructions at top of README 2021-05-24 20:30:31 +01:00
Harvey Tindall
e03525a1d1 separate codenames for stable & unstable
templates don't work in name_template as i though, so this should work
instead.
2021-05-24 19:53:53 +01:00
Harvey Tindall
087172c79e fix package naming to avoid conflicts 2021-05-24 18:46:54 +01:00
Harvey Tindall
8fd919bf04 remove chglog, add steps to upload to apt.hrfee.dev
chglog isn't actually needed. Packages are uploaded as jfa-go(-git) and
jfa-go-tray(-git).
2021-05-24 18:37:26 +01:00
Harvey Tindall
2ad84db482 add inaccurate chglog
not really correct, tagged as v0.3.6 despite the few extra commits.
2021-05-24 16:33:20 +01:00
Harvey Tindall
85536ff79e expand CONTRIBUTING, print if tray enabled on startup 2021-05-24 15:58:43 +01:00
Harvey Tindall
8b62c91d13 Mention TrayIcon deps in README 2021-05-23 23:49:48 +01:00
Harvey Tindall
e7d1693517 Enable updater for Tray builds 2021-05-23 23:20:14 +01:00
Harvey Tindall
e78b4882b3 Fix updater for zip files
Forgot to change this when I switched, oops.
2021-05-23 23:05:40 +01:00
Harvey Tindall
e01144950b Mention Discord in README 2021-05-23 22:41:39 +01:00
Harvey Tindall
86ef665b12 Discord: Try to avoid more race conditions
also added RACE=on to Makefile to enable go's race detector.
2021-05-23 22:26:56 +01:00
Harvey Tindall
f419a57e6d Fixed loaded message, Tray by default 2021-05-23 22:12:47 +01:00
Harvey Tindall
d7e8ec95de add missing perms, fix order 2021-05-23 20:48:17 +01:00
Harvey Tindall
5a9bc1c66f Merge Discord branch
Discord Integration, Accounts UI improvements
2021-05-23 20:25:15 +01:00
Harvey Tindall
1f9af8df89 Discord: Add option to provide server invite
When enabled, a temporary one-use invite is created and shown to the
user on the account creation form.
2021-05-23 19:50:03 +01:00
Harvey Tindall
0676b6c41f Discord: Display channel on account creation form 2021-05-23 17:31:20 +01:00
ClankJake
ac842e6273 translation from Weblate (Portuguese (Brazil))
Currently translated at 100.0% (32 of 32 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/form/pt_BR/
2021-05-23 18:23:21 +02:00
Harvey Tindall
ce8cdced4d Discord: Fix GetUsers, add invite messages
The "Send to" box on the invite tab now accepts username#discriminator,
and a search icon has been added which opens a search window similar to
the one on the accounts tab. DiscordDaemon.GetUsers was also very broken
and wouldn't work with full username#discriminator, that's been fixed.
2021-05-23 16:16:31 +01:00
Harvey Tindall
b8e3fc636c Accounts: Fix cog on telegram when no discord linked
Also, disable telegram & discord if an auth/initialization error occurs.
2021-05-23 14:48:36 +01:00
Harvey Tindall
519a5615cc Accounts: Fix email check on dropdown 2021-05-23 14:35:01 +01:00
Harvey Tindall
168b217553 Discord: fix user links 2021-05-23 14:32:35 +01:00
Harvey Tindall
7d698d63e3 Discord: split discord search into own module
Will also be used for "Send to" on the invite page.
2021-05-23 14:22:18 +01:00
Harvey Tindall
035dbde819 last image 2021-05-23 02:11:48 +01:00
Harvey Tindall
c373d8b2d6 add final oauth tab image 2021-05-23 02:08:02 +01:00
Harvey Tindall
8698c3c6a4 add oauth2 section to bot instructions 2021-05-23 02:03:55 +01:00
Harvey Tindall
0edd2ba68b add settings image for bot setup 2021-05-23 01:51:59 +01:00
Harvey Tindall
b91f0b5a18 Discord: add images for bot creation instructions 2021-05-23 01:38:40 +01:00
Harvey Tindall
24fa841c0d Discord: Wait for non-nil pointer to bot data
While testing others things, I had quite a few nil pointer dereference
errors from accessing bot data right after initializing. A for loop now
waits until the first of the pointers is non-nil, which should
hopefully avoid crashes.
2021-05-23 01:09:03 +01:00
Harvey Tindall
44558b8109 Discord: Remove extra newlines around links
Since links are converted into embeds, links put on their own line often
lead to extra newlines that looks pretty weird. They should now be
stripped.
2021-05-23 01:05:53 +01:00
Harvey Tindall
478b40d0ff Telegram: Escape exclamation marks, remove for images
![alt](link) becomes [alt](link), telegram seems to pick up that they're
images anyway.
2021-05-22 23:38:21 +01:00
Harvey Tindall
8b816dc725 merge translations 2021-05-22 23:26:49 +01:00
Harvey Tindall
81a58f628b Add -H=windowsgui in goreleaser 2021-05-22 23:26:13 +01:00
Harvey Tindall
e98c9b46f1 Accounts: no wrapping for contact dropdown 2021-05-22 23:18:43 +01:00
Harvey Tindall
b3ce7acfcb Accounts: Always inline icons, only one settings cog
Admin chip, email edit bot and contact method cog icon are now always inline.
Only one cog icon is shown now.
2021-05-22 23:05:53 +01:00
Harvey Tindall
9fac79b1f0 Discord: Add users via accounts tab
Doesn't require a PIN like Telegram, as we can access a list of guild
users with the GuildMembers intent set. This has to be enabled under
Bot > Priviliged Gateway intents on the developer portal.
2021-05-22 21:42:15 +01:00
Harvey Tindall
591e3c5ca1 Discord: embed images
![alt](image link) is now converted to an image embed.
2021-05-22 15:32:51 +01:00
Harvey Tindall
35d407afef Discord: remove @ from username 2021-05-22 15:31:25 +01:00
Harvey Tindall
a6447165b7 add email notify enable/disable; remove (de)hyphening
hyphen/dehyphen conflicted with new migration for email contact
preference, and it's been a while since this has been an issue so i've
just commented it out for now.
2021-05-21 22:46:46 +01:00
Malte
23800bb892 Translated using Weblate (German)
Currently translated at 100.0% (7 of 7 strings)

Translation: jfa-go/Password Reset Links
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/password-reset-links/de/
2021-05-21 23:02:50 +02:00
Malte
b47cb91f55 Translated using Weblate (German)
Currently translated at 100.0% (101 of 101 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/setup/de/
2021-05-21 23:02:50 +02:00
Malte
2d9e3fbc1d Translated using Weblate (German)
Currently translated at 100.0% (16 of 16 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/common-strings/de/
2021-05-21 23:02:50 +02:00
Malte
bf67e27737 Translated using Weblate (German)
Currently translated at 100.0% (51 of 51 strings)

Translation: jfa-go/Emails
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/emails/de/
2021-05-21 23:02:50 +02:00
Malte
3427c97e3e translation from Weblate (German)
Currently translated at 100.0% (32 of 32 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/form/de/
2021-05-21 23:02:50 +02:00
Malte
81e69a7166 translation from Weblate (German)
Currently translated at 100.0% (156 of 156 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/admin/de/
2021-05-21 23:02:50 +02:00
Malte
564098b9d8 Added translation using Weblate (German) 2021-05-21 23:02:50 +02:00
ClankJake
ec659174fb translation from Weblate (Portuguese (Brazil))
Currently translated at 100.0% (156 of 156 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/admin/pt_BR/
2021-05-21 23:02:50 +02:00
ClankJake
1a42d8280c Translated using Weblate (Portuguese (Brazil))
Currently translated at 93.7% (15 of 16 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/common-strings/pt_BR/
2021-05-21 23:02:50 +02:00
ClankJake
b14f10d79d translation from Weblate (Portuguese (Brazil))
Currently translated at 100.0% (32 of 32 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/form/pt_BR/
2021-05-21 23:02:50 +02:00
Cornichon420
ee8facd1bf Translated using Weblate (French)
Currently translated at 100.0% (16 of 16 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/common-strings/fr/
2021-05-21 23:02:50 +02:00
Cornichon420
811657b553 Translated using Weblate (French)
Currently translated at 100.0% (51 of 51 strings)

Translation: jfa-go/Emails
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/emails/fr/
2021-05-21 23:02:50 +02:00
Cornichon420
95936f7c29 translation from Weblate (French)
Currently translated at 100.0% (32 of 32 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/form/fr/
2021-05-21 23:02:50 +02:00
Cornichon420
613d4cd9af translation from Weblate (French)
Currently translated at 100.0% (156 of 156 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/admin/fr/
2021-05-21 23:02:50 +02:00
Richard de Boer
7beb3d9974 Translated using Weblate (Dutch)
Currently translated at 100.0% (16 of 16 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/common-strings/nl/
2021-05-21 23:02:50 +02:00
Richard de Boer
6f2bb7f0b5 Translated using Weblate (Dutch)
Currently translated at 100.0% (51 of 51 strings)

Translation: jfa-go/Emails
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/emails/nl/
2021-05-21 23:02:50 +02:00
Richard de Boer
315b5fda91 translation from Weblate (Dutch)
Currently translated at 100.0% (32 of 32 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/form/nl/
2021-05-21 23:02:50 +02:00
Richard de Boer
a6aa89e502 translation from Weblate (Dutch)
Currently translated at 100.0% (156 of 156 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/admin/nl/
2021-05-21 23:02:50 +02:00
Harvey Tindall
3bf722c5fe Discord: send links as embeds
Kind of janky but works. This kind of messes up the layout if you write
links in-line.
2021-05-21 21:59:50 +01:00
Harvey Tindall
e931c09a34 add message when web ui is loaded
a lack of output after "Loading routes" was a little confusing.
2021-05-21 21:39:32 +01:00
Harvey Tindall
f8f5f35cc1 PIN verification, notifications, multiple notif providers
Discord, Email & Telegram can be enabled, although email is always
enabled right now (will fix). Also apparently markdown hyperlinks don't
work in Discord, eventually will implement something to convert them to
embeds.
2021-05-21 21:35:25 +01:00
Harvey Tindall
524941da0c fix heading size with sm 2021-05-21 21:10:32 +01:00
Harvey Tindall
22bba922f9 Discord: Add !lang command 2021-05-18 18:41:42 +01:00
Harvey Tindall
d928df7ab2 Discord: Start bot, add !start and pin validity check
The bot should be created by the admin and added to a discord server
mutual to the intended new user(s). On !start in the server,
communication is moved to DMs. Currently !start works, and validity of a
given PIN is checked although nothing it done with this yet.
2021-05-17 23:42:33 +01:00
Harvey Tindall
4b11bbe21f remove leaked telegram token
token has been revoked, but it doesn't look like it was used anyway.
2021-05-17 11:43:58 +01:00
Harvey Tindall
18bcd55972 remove debug println 2021-05-17 01:32:22 +01:00
Harvey Tindall
057f306ed9 hide/ignore ssl_cert when on windows
x509.SystemCertPool is unavailable on windows, so any value is ignored
and the setting is hidden on the web UI.
2021-05-17 01:16:59 +01:00
Harvey Tindall
76bbb3f147 consistent naming for tray builds 2021-05-16 23:02:09 +01:00
Harvey Tindall
0f3ad8bb69 fix generate_ini for multiline descriptions 2021-05-16 23:00:37 +01:00
Harvey Tindall
1d47b9074f change notray/tray naming, add deb/rpm/apk
Since Tray support requires dependencies, it won't be the default for
releases. deb/rpm/apk support may be broken still.
2021-05-16 22:44:04 +01:00
Harvey Tindall
5167fde080 change tar.gz to zip in drone 2021-05-16 21:12:18 +01:00
Harvey Tindall
a62648ee68 fix cross compilation in goreleaser/drone
Necessary for go-autostart to work on windows. Tray will be enabled by
default for x86_64 windows/linux binaries.
2021-05-16 21:01:31 +01:00
Harvey Tindall
5dee414596 add "autostart on login" option to tray 2021-05-16 17:40:03 +01:00
Harvey Tindall
8cf9b1f905 add basic tray functionality
enable with `make TRAY=on ...`. Cross compilation apparently should work
from linux to linux & windows.
2021-05-16 16:23:28 +01:00
Harvey Tindall
6bf1920160 merge dependabot bump 2021-05-16 15:00:30 +01:00
Harvey Tindall
33f8070e57 cleanup; fix stripping with DEBUG=on 2021-05-16 15:00:13 +01:00
Harvey Tindall
4d2a018032 Merge pull request #98 from hrfee/dependabot/npm_and_yarn/lodash-4.17.21
Bump lodash from 4.17.20 to 4.17.21
2021-05-14 14:48:41 +01:00
dependabot[bot]
ca7fb540ee Bump lodash from 4.17.20 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-11 21:24:34 +00:00
ClankJake
beb0712ce9 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (101 of 101 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/setup/pt_BR/
2021-05-09 20:59:31 +02:00
woosade
a081b14794 Translated using Weblate (Spanish)
Currently translated at 100.0% (7 of 7 strings)

Translation: jfa-go/Password Reset Links
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/password-reset-links/es/
2021-05-09 20:59:30 +02:00
woosade
e416acf6bd translation from Weblate (Spanish)
Currently translated at 100.0% (152 of 152 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/admin/es/
2021-05-09 20:59:30 +02:00
woosade
bf94f76509 Translated using Weblate (Spanish)
Currently translated at 100.0% (101 of 101 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/setup/es/
2021-05-09 20:59:30 +02:00
Richard de Boer
ac239a309c translation from Weblate (Dutch)
Currently translated at 100.0% (152 of 152 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/admin/nl/
2021-05-09 20:59:30 +02:00
Richard de Boer
0f12586166 Translated using Weblate (Dutch)
Currently translated at 100.0% (101 of 101 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/setup/nl/
2021-05-09 20:59:30 +02:00
Cornichon420
b1b50ce561 Translated using Weblate (French)
Currently translated at 100.0% (101 of 101 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/setup/fr/
2021-05-09 20:59:30 +02:00
Cornichon420
8e2bf48ab4 Translated using Weblate (French)
Currently translated at 100.0% (13 of 13 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/common-strings/fr/
2021-05-09 20:59:30 +02:00
Cornichon420
6ec5022a0d translation from Weblate (French)
Currently translated at 100.0% (28 of 28 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/form/fr/
2021-05-09 20:59:30 +02:00
Cornichon420
ef97e0ac76 translation from Weblate (French)
Currently translated at 100.0% (152 of 152 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.hrfee.dev/projects/jfa-go/admin/fr/
2021-05-09 20:59:30 +02:00
Harvey Tindall
30736a055d add example bot settings for wiki 2021-05-08 16:37:40 +01:00
Harvey Tindall
d0905a29be add example bot creation for wiki 2021-05-08 16:29:16 +01:00
Harvey Tindall
fe5cf69b7a Merge Telegram support
For #94.
2021-05-08 16:15:41 +01:00
111 changed files with 9299 additions and 2674 deletions

View File

@@ -9,23 +9,28 @@ steps:
commands:
- git fetch --tags
- name: release
image: golang:latest
image: hrfee/jfa-go-build-docker:latest
volumes:
- name: ssh_key
path: /id_rsa
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
GITHUB_TOKEN:
from_secret: github_token
commands:
- apt-get update -y
- apt-get install build-essential python3-pip curl software-properties-common sed upx -y
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
- apt-get install nodejs
- curl -sL https://git.io/goreleaser > ../goreleaser
- chmod +x ../goreleaser
- ./scripts/version.sh ../goreleaser
- wget https://builds.hrfee.pw/upload.py -P ../
- pip3 install requests
- bash -c 'sftp -P 2022 -i /id_rsa -o StrictHostKeyChecking=no root@161.97.102.153:/repo/incoming <<< $"put dist/*.deb"'
- bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "repo-process-deb trusty"'
- bash -c 'python3 ../upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true'
volumes:
- name: ssh_key
host:
path: /root/.ssh/id_rsa_packaging
trigger:
event:
- tag
@@ -71,22 +76,29 @@ type: docker
steps:
- name: build
image: golang:latest
image: hrfee/jfa-go-build-docker:latest
volumes:
- name: ssh_key
path: /id_rsa
commands:
- apt-get update -y
- apt-get install build-essential python3-pip curl software-properties-common sed upx -y
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
- apt-get install nodejs
- curl -sL https://git.io/goreleaser > goreleaser
- chmod +x goreleaser
- ./scripts/version.sh ./goreleaser --snapshot --skip-publish --rm-dist
- wget https://builds.hrfee.pw/upload.py
- pip3 install requests
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.tar.gz --tag internal-git=true'
- bash -c 'sftp -P 2022 -i /id_rsa -o StrictHostKeyChecking=no root@161.97.102.153:/repo/incoming <<< $"put dist/*.deb"'
# - bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "reprepro -Vb /repo remove trusty-unstable jfa-go"'
# - bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "reprepro -Vb /repo remove trusty-unstable jfa-go-tray"'
- bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "repo-process-deb trusty"'
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.zip ./dist/*.rpm ./dist/*.apk --tag internal-git=true'
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
volumes:
- name: ssh_key
host:
path: /root/.ssh/id_rsa_packaging
trigger:
branch:
- main
@@ -144,7 +156,7 @@ steps:
image: golang:latest
commands:
- apt-get update -y
- apt-get install build-essential python3-pip curl software-properties-common sed upx -y
- apt-get install build-essential python3-pip curl software-properties-common sed upx gcc libgtk-3-dev libappindicator3-dev gcc-mingw-w64-x86-64 -y
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
- apt-get install nodejs
- curl -sL https://git.io/goreleaser > goreleaser

View File

@@ -19,8 +19,11 @@ What to do to reproduce the problem.
**Logs**
**If you're using a build with a tray icon, right-click on it and press "Open logs" to access your logs.**
When you notice the problem, check the output of `jfa-go`. If the problem is not obvious (e.g a panic (red text) or 'ERROR' log), re-run jfa-go with the `-debug` argument and reproduce the problem. You should then take a screenshot of the output, or paste it here, preferably between \`\`\` tags (e.g \`\`\``Log here`\`\`\`). Remember to censor any personal information.
If nothing catches your eye in the log, access the admin page via your browser, go into the console (Right click > Inspect Element > Console), refresh, reproduce the problem then paste the output here in the same way as above.
**Configuration**

View File

@@ -25,24 +25,81 @@ before:
- npx esbuild --bundle ts/pwr.ts --outfile=./data/web/js/pwr.js --minify
- npx esbuild --bundle ts/form.ts --outfile=./data/web/js/form.js --minify
- npx esbuild --bundle ts/setup.ts --outfile=./data/web/js/setup.js --minify
- npx esbuild --bundle ts/crash.ts --outfile=./data/crash.js --minify
- cp html/crash.html data/
- npx uncss data/crash.html --csspath web/css --output data/bundle.css
- npx inline-source --root data data/crash.html data/crash.html
- rm data/bundle.css
- mv data/crash.html data/html/
- go get -u github.com/swaggo/swag/cmd/swag
- swag init -g main.go
builds:
- dir: ./
- id: notray
dir: ./
env:
- CGO_ENABLED=0
ldflags:
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm
- arm64
- amd64
- id: windows-tray
dir: ./
env:
- CGO_ENABLED=1
- CC=x86_64-w64-mingw32-gcc
- CXX=x86_64-w64-mingw32-g++
flags:
- -tags=tray
ldflags:
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary -H=windowsgui
goos:
- windows
goarch:
- amd64
- id: linux-tray
dir: ./
env:
- CGO_ENABLED=1
flags:
- -tags=tray
ldflags:
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary
goos:
- linux
goarch:
- amd64
archives:
- replacements:
- id: windows-tray
builds:
- windows-tray
format: zip
name_template: "{{ .ProjectName }}_{{ .Version }}_TrayIcon_{{ .Os }}_{{ .Arch }}"
replacements:
darwin: macOS
linux: Linux
windows: Windows
amd64: x86_64
- id: linux-tray
builds:
- linux-tray
format: zip
name_template: "{{ .ProjectName }}_{{ .Version }}_TrayIcon_{{ .Os }}_{{ .Arch }}"
replacements:
darwin: macOS
linux: Linux
windows: Windows
amd64: x86_64
- id: notray
builds:
- notray
format: zip
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
replacements:
darwin: macOS
linux: Linux
windows: Windows
@@ -50,10 +107,61 @@ archives:
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "git-{{.ShortCommit}}"
name_template: "0.0.0-{{ .Env.JFA_GO_NFPM_EPOCH }}"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
nfpms:
- id: notray
file_name_template: '{{ .ProjectName }}{{ if .IsSnapshot }}-git{{ end }}_{{ .Arch }}_{{ if .IsSnapshot }}{{ .ShortCommit }}{{ else }}v{{ .Version }}{{ end }}'
package_name: jfa-go
homepage: https://github.com/hrfee/jfa-go
description: A web app for managing users on Jellyfin
maintainer: Harvey Tindall <hrfee@hrfee.dev>
license: MIT
vendor: hrfee.dev
version_metadata: git
builds:
- notray
contents:
- src: ./LICENSE
dst: /usr/share/licenses/jfa-go
formats:
- apk
- deb
- rpm
- id: tray
file_name_template: '{{ .ProjectName }}{{ if .IsSnapshot }}-git{{ end }}_TrayIcon_{{ .Arch }}_{{ if .IsSnapshot }}{{ .ShortCommit }}{{ else }}v{{ .Version }}{{ end }}'
package_name: jfa-go-tray
homepage: https://github.com/hrfee/jfa-go
description: A web app for managing users on Jellyfin
maintainer: Harvey Tindall <hrfee@hrfee.dev>
license: MIT
vendor: hrfee.dev
version_metadata: git
builds:
- linux-tray
contents:
- src: ./LICENSE
dst: /usr/share/licenses/jfa-go
formats:
- apk
- deb
- rpm
overrides:
deb:
conflicts:
- jfa-go
replaces:
- jfa-go
dependencies:
- libappindicator3-1
rpm:
dependencies:
- libappindicator-gtk3
apk:
dependencies:
- libappindicator

View File

@@ -1,6 +1,11 @@
#### Code
I use 4 spaces for indentation. Go should ideally be formatted with `goimports` and/or `gofmt`. I don't use a formatter on typescript, so don't worry about that.
Code in Go should ideally use `PascalCase` for exported values, and `camelCase` for non-exported, JSON for transferring data should use `snake_case`, and Typescript should use `camelCase`. Forgive me for my many inconsistencies in this, and feel free to fix them if you want.
Functions in Go that need to access `*appContext` should be generally be receivers, except when the behaviour could be seen as somewhat independent from it (`email.go` is the best example, its behaviour is broadly independent from the main app except from a couple config values).
#### Compiling
Prefix each of these with `make DEBUG=on INTERNAL=off `:

View File

@@ -27,17 +27,38 @@ else
TAGS := -tags external
endif
TRAY ?= off
ifeq ($(INTERNAL)$(TRAY), offon)
TAGS := $(TAGS) tray
else ifeq ($(INTERNAL)$(TRAY), onon)
TAGS := -tags tray
endif
OS := $(shell go env GOOS)
ifeq ($(TRAY)$(OS), onwindows)
LDFLAGS := $(LDFLAGS) -H=windowsgui
endif
DEBUG ?= off
ifeq ($(DEBUG), on)
LDFLAGS := -s -w $(LDFLAGS)
SOURCEMAP := --sourcemap
TYPECHECK := tsc -noEmit --project ts/tsconfig.json
# jank
COPYTS := rm -r $(DATA)/web/js/ts; cp -r ts $(DATA)/web/js
UNCSS := cp $(DATA)/web/css/bundle.css $(DATA)/bundle.css
else
LDFLAGS := -s -w $(LDFLAGS)
SOURCEMAP :=
COPYTS :=
TYPECHECK :=
UNCSS := npx uncss $(DATA)/crash.html --csspath web/css --output $(DATA)/bundle.css
endif
RACE ?= off
ifeq ($(RACE), on)
RACEDETECTOR := -race
else
RACEDETECTOR :=
endif
npm:
@@ -68,6 +89,7 @@ typescript:
-$(ESBUILD) --bundle ts/pwr.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr.js --minify
-$(ESBUILD) --bundle ts/form.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/form.js --minify
-$(ESBUILD) --bundle ts/setup.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/setup.js --minify
-$(ESBUILD) --bundle ts/crash.ts --outfile=./$(DATA)/crash.js --minify
$(COPYTS)
swagger:
@@ -79,7 +101,7 @@ compile:
$(GOBINARY) mod download
$(info Building)
mkdir -p build
CGO_ENABLED=0 $(GOBINARY) build -ldflags="-s -w $(LDFLAGS)" $(TAGS) -o build/jfa-go
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o build/jfa-go
compress:
upx --lzma build/jfa-go
@@ -89,11 +111,18 @@ bundle-css:
$(info bundling css)
$(ESBUILD) --bundle css/base.css --outfile=$(DATA)/web/css/bundle.css --external:remixicon.css --minify
inline:
cp html/crash.html $(DATA)/crash.html
$(UNCSS)
npx inline-source --root $(DATA) $(DATA)/crash.html $(DATA)/crash.html
rm $(DATA)/bundle.css
copy:
$(info copying fonts)
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/
$(info copying html)
cp -r html $(DATA)/
mv $(DATA)/crash.html $(DATA)/html/
$(info copying static data)
-mkdir -p $(DATA)/web
cp -r static/* $(DATA)/web/
@@ -122,4 +151,4 @@ clean:
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
go clean
all: configuration npm email typescript bundle-css swagger copy compile
all: configuration npm email typescript bundle-css inline swagger copy compile

View File

@@ -4,12 +4,15 @@
[![Translation status](https://weblate.hrfee.pw/widgets/jfa-go/-/svg-badge.svg)](https://weblate.hrfee.pw/engage/jfa-go/)
##### Downloads:
##### [dockerhub](https://hub.docker.com/r/hrfee/jfa-go) | [stable](https://github.com/hrfee/jfa-go/releases) | [nightly](https://builds.hrfee.pw/view/hrfee/jfa-go) | [aur stable](https://aur.archlinux.org/packages/jfa-go) | [aur binary](https://aur.archlinux.org/packages/jfa-go-bin) | [aur nightly](https://aur.archlinux.org/packages/jfa-go-git)
##### [docker](#docker) | [debian/ubuntu](#debian) | [arch (aur)](#aur) | [other platforms](#other-platforms)
---
jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) (and now [Emby](https://emby.media/)) that provides invite-based account creation as well as other features that make one's instance much easier to manage.
a rewrite of [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) (original naming for both, ik
😂).
#### Features
* 🧑 Invite based account creation: Sends invites to your friends or family, and let them choose their own username and password without relying on you.
* 🧑 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
* 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.
@@ -17,7 +20,7 @@ jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jelly
* ⌛ User expiry: Specify a validity period, and new users accounts will be disabled/deleted after it. The period can be manually extended too.
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
* Telegram Integration: Verify users via telegram, and send Password Resets, Announcements, etc. through it.
* Telegram/Discord/Matrix Integration: Verify users via a chat bot, and send Password Resets, Announcements, etc. through it.
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
* Email addresses can optionally be used instead of usernames
* 🔑 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 the user via email/telegram.
@@ -43,7 +46,9 @@ jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jelly
#### Install
The [Docker](https://hub.docker.com/r/hrfee/jfa-go) image is your best bet.
**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.
##### [Docker](https://hub.docker.com/r/hrfee/jfa-go)
```sh
docker create \
--name "jfa-go" \ # Whatever you want to name it
@@ -54,9 +59,41 @@ docker create \
-v /etc/localtime:/etc/localtime:ro \ # Makes sure time is correct
hrfee/jfa-go # hrfee/jfa-go:unstable for latest build from git
```
Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/), [jfa-go-bin](https://aur.archlinux.org/packages/jfa-go) or [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/).
For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.dev/view/hrfee/jfa-go)), and extract the `jfa-go` executable to somewhere useful.
##### [Debian/Ubuntu](https://apt.hrfee.dev)
```sh
sudo apt-get update && sudo apt-get install curl apt-transport-https gnupg
curl https://apt.hrfee.dev/hrfee.pubkey.gpg | sudo apt-key add -
# For stable releases
echo "deb https://apt.hrfee.dev trusty main" | sudo tee /etc/apt/sources.list.d/hrfee.list
# ------
# For unstable releases
echo "deb https://apt.hrfee.dev trusty-unstable main" | sudo tee /etc/apt/sources.list.d/hrfee.list
# ------
sudo apt-get update
# For servers
sudo apt-get install jfa-go
# ------
# For desktops/servers with GUI (has dependencies)
sudo apt-get install jfa-go-tray
# ------
```
##### Arch
Available on the AUR as:
* [jfa-go](https://aur.archlinux.org/packages/jfa-go/) (stable)
* [jfa-go-bin](https://aur.archlinux.org/packages/jfa-go) (pre-compiled, stable)
* [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/) (nightly)
##### Other platforms
Download precompiled binaries from:
* [The releases section](https://github.com/hrfee/jfa-go/releases) (stable)
* [Buildrone](https://builds.hrfee.dev/view/hrfee/jfa-go) (nightly)
unzip the `jfa-go`/`jfa-go.exe` executable to somewhere useful.
* For \*nix/macOS users, `chmod +x jfa-go` then place it somewhere in your PATH like `/usr/bin`.
Run the executable to start.
@@ -71,17 +108,28 @@ Otherwise, full build instructions can be found [here](https://github.com/hrfee/
Simply run `jfa-go` to start the application. A setup wizard will start on `localhost:8056` (or your own specified address). Upon completion, refresh the page.
```
Usage of ./jfa-go:
-config string
alternate path to config file. (default "~/.config/jfa-go/config.ini")
-data string
alternate path to data directory. (default "~/.config/jfa-go")
Usage of jfa-go:
start
start jfa-go as a daemon and run in the background.
stop
stop a daemonized instance of jfa-go.
systemd
generate a systemd .service file.
-config, -c string
alternate path to config file. (default "/home/hrfee/.config/jfa-go/config.ini")
-data, -d string
alternate path to data directory. (default "/home/hrfee/.config/jfa-go")
-debug
Enables debug logging and exposes pprof.
Enables debug logging.
-help, -h
prints this message.
-host string
alternate address to host web ui on.
-port int
-port, -p int
alternate port to host web ui on.
-pprof
Exposes pprof profiler on /debug/pprof.
-swagger
Enable swagger at /swagger/index.html
```

578
api.go
View File

@@ -217,7 +217,7 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
username := jfUser.Name
email := ""
if e, ok := app.storage.emails[jfID]; ok {
email = e.(string)
email = e.Addr
}
for _, ombiUser := range ombiUsers {
ombiAddr := ""
@@ -283,7 +283,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
}
app.jf.CacheExpiry = time.Now()
if emailEnabled {
app.storage.emails[id] = req.Email
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
app.storage.storeEmails()
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
@@ -330,6 +330,58 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false
return
}
var discordUser DiscordUser
discordVerified := false
if discordEnabled {
if req.DiscordPIN == "" {
if app.config.Section("discord").Key("required").MustBool(false) {
f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Discord verification not completed", req.Code)
respond(401, "errorDiscordVerification", gc)
}
success = false
return
}
} else {
discordUser, discordVerified = app.discord.verifiedTokens[req.DiscordPIN]
if !discordVerified {
f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Discord PIN was invalid", req.Code)
respond(401, "errorInvalidPIN", gc)
}
success = false
return
}
}
}
var matrixUser MatrixUser
matrixVerified := false
if matrixEnabled {
if req.MatrixPIN == "" {
if app.config.Section("matrix").Key("required").MustBool(false) {
f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Matrix verification not completed", req.Code)
respond(401, "errorMatrixVerification", gc)
}
success = false
return
}
} else {
user, ok := app.matrix.tokens[req.MatrixPIN]
if !ok || !user.Verified {
matrixVerified = false
f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Matrix PIN was invalid", req.Code)
respond(401, "errorInvalidPIN", gc)
}
success = false
return
}
matrixVerified = user.Verified
matrixUser = *user.User
}
}
telegramTokenIndex := -1
if telegramEnabled {
if req.TelegramPIN == "" {
@@ -454,7 +506,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
}
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
if req.Email != "" {
app.storage.emails[id] = req.Email
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
app.storage.storeEmails()
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
@@ -479,7 +531,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
app.err.Printf("Failed to store user duration: %v", err)
}
}
if discordEnabled && discordVerified {
discordUser.Contact = req.DiscordContact
if app.storage.discord == nil {
app.storage.discord = map[string]DiscordUser{}
}
app.storage.discord[user.ID] = discordUser
if err := app.storage.storeDiscordUsers(); err != nil {
app.err.Printf("Failed to store Discord users: %v", err)
} else {
delete(app.discord.verifiedTokens, req.DiscordPIN)
}
}
if telegramEnabled && telegramTokenIndex != -1 {
tgToken := app.telegram.verifiedTokens[telegramTokenIndex]
tgUser := TelegramUser{
@@ -494,16 +557,25 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
app.storage.telegram = map[string]TelegramUser{}
}
app.storage.telegram[user.ID] = tgUser
err := app.storage.storeTelegramUsers()
if err != nil {
if err := app.storage.storeTelegramUsers(); err != nil {
app.err.Printf("Failed to store Telegram users: %v", err)
} else {
app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[telegramTokenIndex] = app.telegram.verifiedTokens[telegramTokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1]
}
}
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 {
if matrixVerified {
matrixUser.Contact = req.MatrixContact
delete(app.matrix.tokens, req.MatrixPIN)
if app.storage.matrix == nil {
app.storage.matrix = map[string]MatrixUser{}
}
app.storage.matrix[user.ID] = matrixUser
if err := app.storage.storeMatrixUsers(); err != nil {
app.err.Printf("Failed to store Matrix users: %v", err)
}
}
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 || discordVerified {
name := app.getAddressOrName(user.ID)
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
@@ -792,18 +864,44 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
invite.UserMinutes = req.UserMinutes
}
invite.ValidTill = validTill
if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
app.debug.Printf("%s: Sending invite email", inviteCode)
invite.Email = req.Email
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
if err != nil {
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
app.err.Printf("%s: Failed to construct invite email: %v", inviteCode, err)
} else if err := app.email.send(msg, req.Email); err != nil {
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
app.err.Printf("%s: %s: %v", inviteCode, invite.Email, err)
} else {
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.Email)
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
addressValid := false
discord := ""
app.debug.Printf("%s: Sending invite message", inviteCode)
if discordEnabled && !strings.Contains(req.SendTo, "@") {
users := app.discord.GetUsers(req.SendTo)
if len(users) == 0 {
invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
} else if len(users) > 1 {
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", 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(inviteCode, invite, app, false)
if err != nil {
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: Failed to construct invite message: %v", inviteCode, 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("Failed to send to %s", req.SendTo)
app.err.Printf("%s: %s: %v", inviteCode, invite.SendTo, err)
} else {
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.SendTo)
}
}
}
}
if req.Profile != "" {
@@ -867,15 +965,15 @@ func (app *appContext) GetInvites(gc *gin.Context) {
if inv.RemainingUses != 0 {
invite.RemainingUses = inv.RemainingUses
}
if inv.Email != "" {
invite.Email = inv.Email
if inv.SendTo != "" {
invite.SendTo = inv.SendTo
}
if len(inv.Notify) != 0 {
var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
app.storage.loadEmails()
if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil {
address = addr.(string)
if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" {
address = addr.Addr
}
} else {
address = app.config.Section("ui").Key("email").String()
@@ -1074,14 +1172,14 @@ func (app *appContext) SetNotify(gc *gin.Context) {
}
var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
var ok bool
address, ok = app.storage.emails[gc.GetString("jfId")].(string)
addr, ok := app.storage.emails[gc.GetString("jfId")]
if !ok {
app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
respond(500, "Missing user email", gc)
return
}
address = addr.Addr
} else {
address = app.config.Section("ui").Key("email").String()
}
@@ -1168,7 +1266,8 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.LastActive = jfUser.LastActivityDate.Unix()
}
if email, ok := app.storage.emails[jfUser.ID]; ok {
user.Email = email.(string)
user.Email = email.Addr
user.NotifyThroughEmail = email.Contact
}
expiry, ok := app.storage.users[jfUser.ID]
if ok {
@@ -1178,6 +1277,15 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.Telegram = tgUser.Username
user.NotifyThroughTelegram = tgUser.Contact
}
if mxUser, ok := app.storage.matrix[jfUser.ID]; ok {
user.Matrix = mxUser.UserID
user.NotifyThroughMatrix = mxUser.Contact
}
if dcUser, ok := app.storage.discord[jfUser.ID]; ok {
user.Discord = dcUser.Username + "#" + dcUser.Discriminator
user.DiscordID = dcUser.ID
user.NotifyThroughDiscord = dcUser.Contact
}
resp.UserList[i] = user
i++
}
@@ -1253,7 +1361,11 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
for _, jfUser := range users {
id := jfUser.ID
if address, ok := req[id]; ok {
app.storage.emails[id] = address
contact := true
if oldAddr, ok := app.storage.emails[id]; ok {
contact = oldAddr.Contact
}
app.storage.emails[id] = EmailAddress{Addr: address, Contact: contact}
if ombiEnabled {
ombiUser, code, err := app.getOmbiUser(id)
if code == 200 && err == nil {
@@ -1271,6 +1383,77 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
respondBool(200, true, gc)
}
// @Summary Resets a user's password with a PIN, and optionally set a new password if given.
// @Produce json
// @Success 200 {object} boolResponse
// @Success 400 {object} PasswordValidation
// @Failure 500 {object} boolResponse
// @Param ResetPasswordDTO body ResetPasswordDTO true "Pin and optional Password."
// @Router /reset [post]
// @tags Other
func (app *appContext) ResetSetPassword(gc *gin.Context) {
var req ResetPasswordDTO
gc.BindJSON(&req)
validation := app.validator.validate(req.Password)
valid := true
for _, val := range validation {
if !val {
valid = false
}
}
if !valid || req.PIN == "" {
// 200 bcs idk what i did in js
app.info.Printf("%s: Password reset failed: Invalid password", req.PIN)
gc.JSON(400, validation)
return
}
resp, status, err := app.jf.ResetPassword(req.PIN)
if status != 200 || err != nil || !resp.Success {
app.err.Printf("Password Reset failed (%d): %v", status, err)
respondBool(status, false, gc)
return
}
if req.Password == "" || len(resp.UsersReset) == 0 {
respondBool(200, false, gc)
return
}
user, status, err := app.jf.UserByName(resp.UsersReset[0], false)
if status != 200 || err != nil {
app.err.Printf("Failed to get user \"%s\" (%d): %v", resp.UsersReset[0], status, err)
respondBool(500, false, gc)
return
}
status, err = app.jf.SetPassword(user.ID, req.PIN, req.Password)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf("Failed to change password for \"%s\" (%d): %v", resp.UsersReset[0], status, err)
respondBool(500, false, gc)
return
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
// Silently fail for changing ombi passwords
if status != 200 || err != nil {
app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", resp.UsersReset[0], status, err)
respondBool(200, true, gc)
return
}
ombiUser, status, err := app.getOmbiUser(user.ID)
if status != 200 || err != nil {
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", resp.UsersReset[0], status, err)
respondBool(200, true, gc)
return
}
ombiUser["password"] = req.Password
status, err = app.ombi.ModifyUser(ombiUser)
if status != 200 || err != nil {
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
respondBool(200, true, gc)
return
}
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
}
respondBool(200, true, gc)
}
// @Summary Apply settings to a list of users, either from a profile or from another user.
// @Produce json
// @Param userSettingsDTO body userSettingsDTO true "Parameters for applying settings"
@@ -1398,6 +1581,16 @@ func (app *appContext) GetConfig(gc *gin.Context) {
}
}
}
if PLATFORM == "windows" {
delete(resp.Sections["smtp"].Settings, "ssl_cert")
for i, v := range resp.Sections["smtp"].Order {
if v == "ssl_cert" {
sect := resp.Sections["smtp"]
sect.Order = append(sect.Order[:i], sect.Order[i+1:]...)
resp.Sections["smtp"] = sect
}
}
}
for sectName, section := range resp.Sections {
for settingName, setting := range section.Settings {
val := app.config.Section(sectName).Key(settingName)
@@ -1418,6 +1611,8 @@ func (app *appContext) GetConfig(gc *gin.Context) {
resp.Sections["email"].Settings["language"] = el
resp.Sections["password_resets"].Settings["language"] = pl
resp.Sections["telegram"].Settings["language"] = tl
resp.Sections["discord"].Settings["language"] = tl
resp.Sections["matrix"].Settings["language"] = tl
gc.JSON(200, resp)
}
@@ -1426,6 +1621,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
// @Produce json
// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings."
// @Success 200 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /config [post]
// @Security Bearer
// @tags Configuration
@@ -1451,14 +1647,20 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
}
}
}
tempConfig.SaveTo(app.configPath)
tempConfig.Section("").Key("first_run").SetValue("false")
if err := tempConfig.SaveTo(app.configPath); err != nil {
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
respondBool(500, false, gc)
return
}
app.debug.Println("Config saved")
gc.JSON(200, map[string]bool{"success": true})
if req["restart-program"] != nil && req["restart-program"].(bool) {
app.info.Println("Restarting...")
err := app.Restart()
if err != nil {
app.err.Printf("Couldn't restart, try restarting manually: %s", err)
if TRAY {
TRAYRESTART <- true
} else {
RESTART <- true
}
}
app.loadConfig()
@@ -1992,40 +2194,91 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
respondBool(200, true, gc)
}
// @Summary Sets whether to notify a user through telegram or not.
// @Summary Sets whether to notify a user through telegram/discord/matrix/email or not.
// @Produce json
// @Param telegramNotifyDTO body telegramNotifyDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
// @Param SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
// @Success 200 {object} boolResponse
// @Success 400 {object} boolResponse
// @Success 500 {object} boolResponse
// @Router /users/telegram/notify [post]
// @Security Bearer
// @tags Other
func (app *appContext) TelegramSetNotify(gc *gin.Context) {
var req telegramNotifyDTO
func (app *appContext) SetContactMethods(gc *gin.Context) {
var req SetContactMethodsDTO
gc.BindJSON(&req)
if req.ID == "" {
respondBool(400, false, gc)
return
}
if tgUser, ok := app.storage.telegram[req.ID]; ok {
tgUser.Contact = req.Enabled
change := tgUser.Contact != req.Telegram
tgUser.Contact = req.Telegram
app.storage.telegram[req.ID] = tgUser
if err := app.storage.storeTelegramUsers(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Telegram: Failed to store users: %v", err)
return
}
respondBool(200, true, gc)
msg := ""
if !req.Enabled {
msg = "not"
if change {
msg := ""
if !req.Telegram {
msg = " not"
}
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
}
app.debug.Printf("Telegram: User \"%s\" will %s be notified through Telegram.", tgUser.Username, msg)
return
}
app.err.Printf("Telegram: User \"%s\" does not have a telegram account registered.", req.ID)
respondBool(400, false, gc)
if dcUser, ok := app.storage.discord[req.ID]; ok {
change := dcUser.Contact != req.Discord
dcUser.Contact = req.Discord
app.storage.discord[req.ID] = dcUser
if err := app.storage.storeDiscordUsers(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Discord: Failed to store users: %v", err)
return
}
if change {
msg := ""
if !req.Discord {
msg = " not"
}
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
}
}
if mxUser, ok := app.storage.matrix[req.ID]; ok {
change := mxUser.Contact != req.Matrix
mxUser.Contact = req.Matrix
app.storage.matrix[req.ID] = mxUser
if err := app.storage.storeMatrixUsers(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Matrix: Failed to store users: %v", err)
return
}
if change {
msg := ""
if !req.Matrix {
msg = " not"
}
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
}
}
if email, ok := app.storage.emails[req.ID]; ok {
change := email.Contact != req.Email
email.Contact = req.Email
app.storage.emails[req.ID] = email
if err := app.storage.storeEmails(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Failed to store emails: %v", err)
return
}
if change {
msg := ""
if !req.Email {
msg = " not"
}
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
}
}
respondBool(200, true, gc)
}
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
@@ -2082,6 +2335,241 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
respondBool(200, tokenIndex != -1, gc)
}
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Param pin path string true "PIN code to check"
// @Param invCode path string true "invite Code"
// @Router /invite/{invCode}/discord/verified/{pin} [get]
// @tags Other
func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok {
respondBool(401, false, gc)
return
}
pin := gc.Param("pin")
_, ok := app.discord.verifiedTokens[pin]
respondBool(200, ok, gc)
}
// @Summary Returns a 10-minute, one-use Discord server invite
// @Produce json
// @Success 200 {object} DiscordInviteDTO
// @Failure 400 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param invCode path string true "invite Code"
// @Router /invite/{invCode}/discord/invite [get]
// @tags Other
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
if app.discord.inviteChannelName == "" {
respondBool(400, false, gc)
return
}
code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok {
respondBool(401, false, gc)
return
}
invURL, iconURL := app.discord.NewTempInvite(10*60, 1)
if invURL == "" {
respondBool(500, false, gc)
return
}
gc.JSON(200, DiscordInviteDTO{invURL, iconURL})
}
// @Summary Generate and send a new PIN to a specified Matrix user.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param invCode path string true "invite Code"
// @Param MatrixSendPINDTO body MatrixSendPINDTO true "User's Matrix ID."
// @Router /invite/{invCode}/matrix/user [post]
// @tags Other
func (app *appContext) MatrixSendPIN(gc *gin.Context) {
code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok {
respondBool(401, false, gc)
return
}
var req MatrixSendPINDTO
gc.BindJSON(&req)
if req.UserID == "" {
respondBool(400, false, gc)
return
}
ok := app.matrix.SendStart(req.UserID)
if !ok {
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Check whether a matrix PIN is valid, and mark the token as verified if so. Requires invite code.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 401 {object} boolResponse
// @Param pin path string true "PIN code to check"
// @Param invCode path string true "invite Code"
// @Param userID path string true "Matrix User ID"
// @Router /invite/{invCode}/matrix/verified/{userID}/{pin} [get]
// @tags Other
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok {
app.debug.Println("Matrix: Invite code was invalid")
respondBool(401, false, gc)
return
}
userID := gc.Param("userID")
pin := gc.Param("pin")
user, ok := app.matrix.tokens[pin]
if !ok {
app.debug.Println("Matrix: PIN not found")
respondBool(200, false, gc)
return
}
if user.User.UserID != userID {
app.debug.Println("Matrix: User ID of PIN didn't match")
respondBool(200, false, gc)
return
}
user.Verified = true
app.matrix.tokens[pin] = user
respondBool(200, true, gc)
}
// @Summary Generates a Matrix access token from a username and password.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Failure 401 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param MatrixLoginDTO body MatrixLoginDTO true "Username & password."
// @Router /matrix/login [post]
// @tags Other
func (app *appContext) MatrixLogin(gc *gin.Context) {
var req MatrixLoginDTO
gc.BindJSON(&req)
if req.Username == "" || req.Password == "" {
respond(400, "errorLoginBlank", gc)
return
}
token, err := app.matrix.generateAccessToken(req.Homeserver, req.Username, req.Password)
if err != nil {
app.err.Printf("Matrix: Failed to generate token: %v", err)
respond(401, "Unauthorized", gc)
return
}
tempConfig, _ := ini.Load(app.configPath)
matrix := tempConfig.Section("matrix")
matrix.Key("enabled").SetValue("true")
matrix.Key("homeserver").SetValue(req.Homeserver)
matrix.Key("token").SetValue(token)
matrix.Key("user_id").SetValue(req.Username)
if err := tempConfig.SaveTo(app.configPath); err != nil {
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Links a Matrix user to a Jellyfin account via user IDs. Notifications are turned on by default.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param MatrixConnectUserDTO body MatrixConnectUserDTO true "User's Jellyfin ID & Matrix user ID."
// @Router /users/matrix [post]
// @tags Other
func (app *appContext) MatrixConnect(gc *gin.Context) {
var req MatrixConnectUserDTO
gc.BindJSON(&req)
if app.storage.matrix == nil {
app.storage.matrix = map[string]MatrixUser{}
}
roomID, err := app.matrix.CreateRoom(req.UserID)
if err != nil {
app.err.Printf("Matrix: Failed to create room: %v", err)
respondBool(500, false, gc)
return
}
app.storage.matrix[req.JellyfinID] = MatrixUser{
UserID: req.UserID,
RoomID: roomID,
Lang: "en-us",
Contact: true,
}
if err := app.storage.storeMatrixUsers(); err != nil {
app.err.Printf("Failed to store Matrix users: %v", err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional).
// @Produce json
// @Success 200 {object} DiscordUsersDTO
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param username path string true "username to search."
// @Router /users/discord/{username} [get]
// @tags Other
func (app *appContext) DiscordGetUsers(gc *gin.Context) {
name := gc.Param("username")
if name == "" {
respondBool(400, false, gc)
return
}
users := app.discord.GetUsers(name)
resp := DiscordUsersDTO{Users: make([]DiscordUserDTO, len(users))}
for i, u := range users {
resp.Users[i] = DiscordUserDTO{
Name: u.User.Username + "#" + u.User.Discriminator,
ID: u.User.ID,
AvatarURL: u.User.AvatarURL("32"),
}
}
gc.JSON(200, resp)
}
// @Summary Links a Discord account to a Jellyfin account via user IDs. Notifications are turned on by default.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID."
// @Router /users/discord [post]
// @tags Other
func (app *appContext) DiscordConnect(gc *gin.Context) {
var req DiscordConnectUserDTO
gc.BindJSON(&req)
if req.JellyfinID == "" || req.DiscordID == "" {
respondBool(400, false, gc)
return
}
user, ok := app.discord.NewUser(req.DiscordID)
if !ok {
respondBool(500, false, gc)
return
}
app.storage.discord[req.JellyfinID] = user
if err := app.storage.storeDiscordUsers(); err != nil {
app.err.Printf("Failed to store Discord users: %v", err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Restarts the program. No response means success.
// @Router /restart [post]
// @Security Bearer

69
autostart.go Normal file
View File

@@ -0,0 +1,69 @@
// +build tray
package main
import (
"log"
"os"
"path/filepath"
"github.com/emersion/go-autostart"
"github.com/getlantern/systray"
)
type Autostart struct {
as *autostart.App
enabled bool
menuitem *systray.MenuItem
clicked chan bool
}
func NewAutostart(name, displayname, trayName, trayTooltip string) *Autostart {
a := &Autostart{
as: &autostart.App{
Name: name,
DisplayName: displayname,
},
enabled: true,
clicked: make(chan bool),
}
a.menuitem = systray.AddMenuItemCheckbox(trayName, trayTooltip, a.as.IsEnabled())
command := os.Args
command[0], _ = filepath.Abs(command[0])
// Make sure to replace any relative paths with absolute ones
pathArgs := []string{"-d", "-data", "-c", "-config"}
for i := 1; i < len(command); i++ {
isPath := false
for _, p := range pathArgs {
if command[i-1] == p {
isPath = true
break
}
}
if isPath {
command[i], _ = filepath.Abs(command[i])
}
}
a.as.Exec = command
return a
}
func (a *Autostart) HandleCheck() {
for range a.menuitem.ClickedCh {
if !a.menuitem.Checked() {
if err := a.as.Enable(); err != nil {
log.Printf("Failed to enable autostart on login: %v", err)
} else {
a.menuitem.Check()
log.Printf("Enabled autostart")
}
} else {
if err := a.as.Disable(); err != nil {
log.Printf("Failed to disable autostart on login: %v", err)
} else {
a.menuitem.Uncheck()
log.Printf("Disabled autostart")
}
}
}
}

View File

@@ -12,7 +12,7 @@ type TimeoutHandler func()
func NewTimeoutHandler(name, addr string, noFail bool) TimeoutHandler {
return func() {
if r := recover(); r != nil {
out := fmt.Sprintf("Failed to authenticate with %s @ %s: Timed out", name, addr)
out := fmt.Sprintf("Failed to authenticate with %s @ \"%s\": Timed out", name, addr)
if noFail {
log.Print(out)
} else {

View File

@@ -14,6 +14,8 @@ import (
var emailEnabled = false
var messagesEnabled = false
var telegramEnabled = false
var discordEnabled = false
var matrixEnabled = false
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
val := app.config.Section(sect).Key(key).MustString("")
@@ -42,7 +44,7 @@ func (app *appContext) loadConfig() error {
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
}
}
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users"} {
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users"} {
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
}
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
@@ -82,20 +84,26 @@ func (app *appContext) loadConfig() error {
app.MustSetValue("user_expiry", "email_html", "jfa-go:"+"user-expired.html")
app.MustSetValue("user_expiry", "email_text", "jfa-go:"+"user-expired.txt")
app.MustSetValue("matrix", "topic", "Jellyfin notifications")
app.config.Section("jellyfin").Key("version").SetValue(version)
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
discordEnabled = app.config.Section("discord").Key("enabled").MustBool(false)
matrixEnabled = app.config.Section("matrix").Key("enabled").MustBool(false)
if !messagesEnabled {
emailEnabled = false
telegramEnabled = false
discordEnabled = false
matrixEnabled = false
} else if app.config.Section("email").Key("method").MustString("") == "" {
emailEnabled = false
} else {
emailEnabled = true
}
if !emailEnabled && !telegramEnabled {
if !emailEnabled && !telegramEnabled && !discordEnabled && !matrixEnabled {
messagesEnabled = false
}
@@ -143,28 +151,3 @@ func (app *appContext) loadConfig() error {
return nil
}
func (app *appContext) migrateEmailConfig() {
tempConfig, _ := ini.Load(app.configPath)
fmt.Println(warning("Part of your email configuration will be migrated to the new \"messages\" section.\nA backup will be made."))
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
if err != nil {
app.err.Fatalf("Failed to backup config: %v", err)
return
}
for _, setting := range []string{"use_24h", "date_format", "message"} {
if val := app.config.Section("email").Key(setting).Value(); val != "" {
tempConfig.Section("email").Key(setting).SetValue("")
tempConfig.Section("messages").Key(setting).SetValue(val)
}
}
if app.config.Section("messages").Key("enabled").MustBool(false) || app.config.Section("telegram").Key("enabled").MustBool(false) {
tempConfig.Section("messages").Key("enabled").SetValue("true")
}
err = tempConfig.SaveTo(app.configPath)
if err != nil {
app.err.Fatalf("Failed to save config: %v", err)
return
}
app.loadConfig()
}

View File

@@ -546,11 +546,94 @@
}
}
},
"discord": {
"order": [],
"meta": {
"name": "Discord",
"description": "Settings for Discord invites/signup/notifications"
},
"settings": {
"enabled": {
"name": "Enabled",
"required": false,
"requires_restart": true,
"type": "bool",
"value": false,
"description": "Enable signup verification through Discord and the sending of notifications through it.\nSee the jfa-go wiki for setting up a bot."
},
"required": {
"name": "Require on sign-up",
"required": false,
"required_restart": true,
"depends_true": "enabled",
"type": "bool",
"value": false,
"description": "Require Discord connection on sign-up. See the jfa-go wiki for info on setting this up."
},
"token": {
"name": "API Token",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "text",
"value": "",
"description": "Discord Bot API Token."
},
"start_command": {
"name": "Start command",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "text",
"value": "!start",
"description": "Command to start the user verification process."
},
"channel": {
"name": "Channel to monitor",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "text",
"value": "",
"description": "Only listen to commands in specified channel. Leave blank to monitor all."
},
"provide_invite": {
"name": "Provide server invite",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "bool",
"value": false,
"description": "Generate a one-time discord server invite for the account creation form. Required Bot permission \"Create instant invite\", you may need to re-add the bot to your server after."
},
"invite_channel": {
"name": "Invite channel",
"required": false,
"requires_restart": true,
"depends_true": "provide_invite",
"type": "text",
"value": "",
"description": "Channel to invite new users to."
},
"language": {
"name": "Language",
"required": false,
"requires_restart": false,
"depends_true": "enabled",
"type": "select",
"options": [
["en-us", "English (US)"]
],
"value": "en-us",
"description": "Default Discord message language. Visit weblate if you'd like to translate."
}
}
},
"telegram": {
"order": [],
"meta": {
"name": "Telegram",
"description": "Settings for Telegram signup/notifications"
"description": "Settings for Telegram signup/notifications. See the jfa-go wiki for info on setting this up."
},
"settings": {
"enabled": {
@@ -565,6 +648,7 @@
"name": "Require on sign-up",
"required": false,
"required_restart": true,
"depends_true": "enabled",
"type": "bool",
"value": false,
"description": "Require telegram connection on sign-up."
@@ -592,6 +676,80 @@
}
}
},
"matrix": {
"order": [],
"meta": {
"name": "Matrix",
"description": "Settings for Matrix invites/signup/notifications. See the jfa-go wiki for info on setting this up."
},
"settings": {
"enabled": {
"name": "Enabled",
"required": false,
"requires_restart": true,
"type": "bool",
"value": false,
"description": "Enable signup verification through Matrix and the sending of notifications through it.\nSee the jfa-go wiki for setting up a bot."
},
"required": {
"name": "Require on sign-up",
"required": false,
"required_restart": true,
"depends_true": "enabled",
"type": "bool",
"value": false,
"description": "Require Matrix connection on sign-up."
},
"homeserver": {
"name": "Home Server URL",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "text",
"value": "",
"description": "Matrix Home server URL."
},
"token": {
"name": "Access Token",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "text",
"value": "",
"description": "Matrix Bot API Token."
},
"user_id": {
"name": "Bot User ID",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "text",
"value": "",
"description": "User ID of bot account (Example: @jfa-bot:riot.im)"
},
"topic": {
"name": "Chat topic",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "text",
"value": "Jellyfin notifications",
"description": "Topic of Matrix private chats."
},
"language": {
"name": "Language",
"required": false,
"requires_restart": false,
"depends_true": "enabled",
"type": "select",
"options": [
["en-us", "English (US)"]
],
"value": "en-us",
"description": "Default Matrix message language. Visit weblate if you'd like to translate."
}
}
},
"password_resets": {
"order": [],
"meta": {
@@ -626,6 +784,15 @@
"value": false,
"description": "Send users a link to reset their password instead of a PIN. Must be enabled to reset Ombi password at the same time as the Jellyfin password."
},
"set_password": {
"name": "Set password through link",
"required": false,
"requires_restart": true,
"depends_true": "link_reset",
"type": "bool",
"value": false,
"description": "Instead of automatically setting the user's password to the PIN, allow them to set a new password through the reset link."
},
"language": {
"name": "Default reset link language",
"required": false,
@@ -1077,6 +1244,14 @@
"value": "",
"description": "Location of stored invites (json)."
},
"password_resets": {
"name": "Password Resets",
"required": false,
"requires_restart": true,
"type": "text",
"value": "",
"description": "Location of stored non-Jellyfin password resets (json)."
},
"emails": {
"name": "Email Addresses",
"required": false,
@@ -1140,6 +1315,22 @@
"type": "text",
"value": "",
"description": "Stores telegram user IDs and language preferences."
},
"matrix_users": {
"name": "Matrix users",
"required": false,
"requires_restart": false,
"type": "text",
"value": "",
"description": "Stores matrix user IDs and language preferences."
},
"discord_users": {
"name": "Discord users",
"required": false,
"requires_restart": false,
"type": "text",
"value": "",
"description": "Stores discord user IDs and language preferences."
}
}
}

View File

@@ -30,12 +30,12 @@
}
}
@media screen and (max-width: 750px) {
@media screen and (max-width: 1000px) {
:root {
font-size: 0.9rem;
}
.table-responsive table {
min-width: 660px;
min-width: 800px;
}
}
@@ -130,6 +130,10 @@ div.card:contains(section.banner.footer) {
text-align: center;
}
.w-100 {
width: 100%;
}
.inline-block {
display: inline-block;
}
@@ -172,7 +176,7 @@ div.card:contains(section.banner.footer) {
}
p.sm,
span.sm {
span.sm:not(.heading) {
font-size: 0.75rem;
}
@@ -424,6 +428,7 @@ p.top {
.table-responsive {
overflow-x: auto;
font-size: 0.9rem;
}
#notification-box {
@@ -438,6 +443,10 @@ p.top {
margin-bottom: -0.5rem;
}
.dropdown-display.lg {
white-space: nowrap;
}
pre {
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
@@ -483,3 +492,44 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
max-width: 15rem;
min-width: 10rem;
}
td.img-circle {
width: 32px;
height: 32px;
}
span.img-circle.lg {
width: 64px;
height: 64px;
}
span.shield.img-circle {
padding: 0.2rem;
}
img.img-circle {
border-radius: 50%;
vertical-align: middle;
}
.table td.sm {
padding-top: 0.1rem;
padding-bottom: 0.1rem;
}
.table-inline {
display: flex !important;
align-items: center;
}
div.card:contains(section.banner.footer) {
padding-bottom: 0px;
}
.card.sectioned {
padding: 0px;
}
.card.sectioned .section {
padding: var(--spacing-4, 1rem);
}

410
discord.go Normal file
View File

@@ -0,0 +1,410 @@
package main
import (
"fmt"
"strings"
dg "github.com/bwmarrin/discordgo"
)
type DiscordDaemon struct {
Stopped bool
ShutdownChannel chan string
bot *dg.Session
username string
tokens []string
verifiedTokens map[string]DiscordUser // Map of tokens to discord users.
channelID, channelName, inviteChannelID, inviteChannelName string
guildID string
serverChannelName, serverName string
users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start.
app *appContext
}
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
token := app.config.Section("discord").Key("token").String()
if token == "" {
return nil, fmt.Errorf("token was blank")
}
bot, err := dg.New("Bot " + token)
if err != nil {
return nil, err
}
dd := &DiscordDaemon{
Stopped: false,
ShutdownChannel: make(chan string),
bot: bot,
tokens: []string{},
verifiedTokens: map[string]DiscordUser{},
users: map[string]DiscordUser{},
app: app,
}
for _, user := range app.storage.discord {
dd.users[user.ID] = user
}
return dd, nil
}
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
func (d *DiscordDaemon) NewAuthToken() string {
pin := genAuthToken()
d.tokens = append(d.tokens, pin)
return pin
}
func (d *DiscordDaemon) NewUnknownUser(channelID, userID, discrim, username string) DiscordUser {
user := DiscordUser{
ChannelID: channelID,
ID: userID,
Username: username,
Discriminator: discrim,
}
return user
}
func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string) DiscordUser {
if user, ok := d.users[userID]; ok {
return user
}
return d.NewUnknownUser(channelID, userID, discrim, username)
}
func (d *DiscordDaemon) run() {
d.bot.AddHandler(d.messageHandler)
d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers | dg.IntentsGuildInvites
if err := d.bot.Open(); err != nil {
d.app.err.Printf("Discord: Failed to start daemon: %v", err)
return
}
// Wait for everything to populate, it's slow sometimes.
for d.bot.State == nil {
continue
}
for d.bot.State.User == nil {
continue
}
d.username = d.bot.State.User.Username
for d.bot.State.Guilds == nil {
continue
}
// Choose the last guild (server), for now we don't really support multiple anyway
d.guildID = d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID
guild, err := d.bot.Guild(d.guildID)
if err != nil {
d.app.err.Printf("Discord: Failed to get guild: %v", err)
}
d.serverChannelName = guild.Name
d.serverName = guild.Name
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
d.channelName = channel
d.serverChannelName += "/" + channel
}
if d.app.config.Section("discord").Key("provide_invite").MustBool(false) {
if invChannel := d.app.config.Section("discord").Key("invite_channel").String(); invChannel != "" {
d.inviteChannelName = invChannel
}
}
defer d.bot.Close()
<-d.ShutdownChannel
d.ShutdownChannel <- "Down"
return
}
// NewTempInvite creates an invite link, and returns the invite URL, as well as the URL for the server icon.
func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconURL string) {
var inv *dg.Invite
var err error
if d.inviteChannelName == "" {
d.app.err.Println("Discord: Cannot create invite without channel specified in settings.")
return
}
if d.inviteChannelID == "" {
channels, err := d.bot.GuildChannels(d.guildID)
if err != nil {
d.app.err.Printf("Discord: Couldn't get channel list: %v", err)
return
}
found := false
for _, channel := range channels {
// channel, err := d.bot.Channel(ch.ID)
// if err != nil {
// d.app.err.Printf("Discord: Couldn't get channel: %v", err)
// return
// }
if channel.Name == d.inviteChannelName {
d.inviteChannelID = channel.ID
found = true
break
}
}
if !found {
d.app.err.Printf("Discord: Couldn't find invite channel \"%s\"", d.inviteChannelName)
return
}
}
// channel, err := d.bot.Channel(d.inviteChannelID)
// if err != nil {
// d.app.err.Printf("Discord: Couldn't get invite channel: %v", err)
// return
// }
inv, err = d.bot.ChannelInviteCreate(d.inviteChannelID, dg.Invite{
// Guild: d.bot.State.Guilds[len(d.bot.State.Guilds)-1],
// Channel: channel,
// Inviter: d.bot.State.User,
MaxAge: ageSeconds,
MaxUses: maxUses,
Temporary: false,
})
if err != nil {
d.app.err.Printf("Discord: Failed to create invite: %v", err)
return
}
inviteURL = "https://discord.gg/" + inv.Code
guild, err := d.bot.Guild(d.guildID)
if err != nil {
d.app.err.Printf("Discord: Failed to get guild: %v", err)
return
}
iconURL = guild.IconURL()
return
}
// Returns the user(s) roughly corresponding to the username (if they are in the guild).
// if no discriminator (#xxxx) is given in the username and there are multiple corresponding users, a list of all matching users is returned.
func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
members, err := d.bot.GuildMembers(
d.guildID,
"",
1000,
)
if err != nil {
d.app.err.Printf("Discord: Failed to get members: %v", err)
return nil
}
hasDiscriminator := strings.Contains(username, "#")
var users []*dg.Member
for _, member := range members {
if hasDiscriminator {
if member.User.Username+"#"+member.User.Discriminator == username {
return []*dg.Member{member}
}
}
if strings.Contains(member.User.Username, username) {
users = append(users, member)
}
}
return users
}
func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) {
u, err := d.bot.User(ID)
if err != nil {
d.app.err.Printf("Discord: Failed to get user: %v", err)
return
}
user.ID = ID
user.Username = u.Username
user.Contact = true
user.Discriminator = u.Discriminator
channel, err := d.bot.UserChannelCreate(ID)
if err != nil {
d.app.err.Printf("Discord: Failed to create DM channel: %v", err)
return
}
user.ChannelID = channel.ID
ok = true
return
}
func (d *DiscordDaemon) Shutdown() {
d.Stopped = true
d.ShutdownChannel <- "Down"
<-d.ShutdownChannel
close(d.ShutdownChannel)
}
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
if m.GuildID != "" && d.channelName != "" {
if d.channelID == "" {
channel, err := s.Channel(m.ChannelID)
if err != nil {
d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
d.channelName = ""
}
if channel.Name == d.channelName {
d.channelID = channel.ID
}
}
if d.channelID != m.ChannelID {
d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
return
}
}
if m.Author.ID == s.State.User.ID {
return
}
sects := strings.Split(m.Content, " ")
if len(sects) == 0 {
return
}
lang := d.app.storage.lang.chosenTelegramLang
if user, ok := d.users[m.Author.ID]; ok {
if _, ok := d.app.storage.lang.Telegram[user.Lang]; ok {
lang = user.Lang
}
}
switch msg := sects[0]; msg {
case d.app.config.Section("discord").Key("start_command").MustString("!start"):
d.commandStart(s, m, lang)
case "!lang":
d.commandLang(s, m, sects, lang)
default:
d.commandPIN(s, m, sects, lang)
}
}
func (d *DiscordDaemon) commandStart(s *dg.Session, m *dg.MessageCreate, lang string) {
channel, err := s.UserChannelCreate(m.Author.ID)
if err != nil {
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", m.Author.Username, err)
return
}
user := d.MustGetUser(channel.ID, m.Author.ID, m.Author.Discriminator, m.Author.Username)
d.users[m.Author.ID] = user
content := d.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
content += d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"})
_, err = s.ChannelMessageSend(channel.ID, content)
if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
return
}
}
func (d *DiscordDaemon) commandLang(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
if len(sects) == 1 {
list := "!lang <lang>\n"
for code := range d.app.storage.lang.Telegram {
list += fmt.Sprintf("%s: %s\n", code, d.app.storage.lang.Telegram[code].Meta.Name)
}
_, err := s.ChannelMessageSendReply(
m.ChannelID,
list,
m.Reference(),
)
if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
}
return
}
if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
var user DiscordUser
for jfID, user := range d.app.storage.discord {
if user.ID == m.Author.ID {
user.Lang = sects[1]
d.app.storage.discord[jfID] = user
if err := d.app.storage.storeDiscordUsers(); err != nil {
d.app.err.Printf("Failed to store Discord users: %v", err)
}
break
}
}
d.users[m.Author.ID] = user
}
}
func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
if _, ok := d.users[m.Author.ID]; ok {
channel, err := s.Channel(m.ChannelID)
if err != nil {
d.app.err.Printf("Discord: Failed to get channel: %v", err)
return
}
if channel.Type != dg.ChannelTypeDM {
d.app.debug.Println("Discord: Ignoring message as not a DM")
return
}
} else {
d.app.debug.Println("Discord: Ignoring message as user was not found")
return
}
tokenIndex := -1
for i, token := range d.tokens {
if sects[0] == token {
tokenIndex = i
break
}
}
if tokenIndex == -1 {
_, err := s.ChannelMessageSend(
m.ChannelID,
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
)
if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
}
return
}
_, err := s.ChannelMessageSend(
m.ChannelID,
d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"),
)
if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
}
d.verifiedTokens[sects[0]] = d.users[m.Author.ID]
d.tokens[len(d.tokens)-1], d.tokens[tokenIndex] = d.tokens[tokenIndex], d.tokens[len(d.tokens)-1]
d.tokens = d.tokens[:len(d.tokens)-1]
}
func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error {
channels := make([]string, len(userID))
for i, id := range userID {
channel, err := d.bot.UserChannelCreate(id)
if err != nil {
return err
}
channels[i] = channel.ID
}
return d.Send(message, channels...)
}
func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
msg := ""
var embeds []*dg.MessageEmbed
if message.Markdown != "" {
msg, embeds = StripAltText(message.Markdown, true)
} else {
msg = message.Text
}
for _, id := range channelID {
var err error
if len(embeds) != 0 {
_, err = d.bot.ChannelMessageSendComplex(
id,
&dg.MessageSend{
Content: msg,
Embed: embeds[0],
},
)
if err != nil {
return err
}
for i := 1; i < len(embeds); i++ {
_, err := d.bot.ChannelMessageSendEmbed(id, embeds[i])
if err != nil {
return err
}
}
} else {
_, err := d.bot.ChannelMessageSend(
id,
msg,
)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -25,6 +25,8 @@ import (
"github.com/mailgun/mailgun-go/v4"
)
var renderer = html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
// implements email sending, right now via smtp or mailgun.
type EmailClient interface {
Send(fromName, fromAddr string, message *Message, address ...string) error
@@ -177,6 +179,20 @@ func (emailer *Emailer) NewMailgun(url, key string) {
// NewSMTP returns an SMTP emailClient.
func (emailer *Emailer) NewSMTP(server string, port int, username, password string, sslTLS bool, certPath string) (err error) {
// x509.SystemCertPool is unavailable on windows
if PLATFORM == "windows" {
emailer.sender = &SMTP{
auth: smtp.PlainAuth("", username, password, server),
server: server,
port: port,
sslTLS: sslTLS,
tlsConfig: &tls.Config{
InsecureSkipVerify: false,
ServerName: server,
},
}
return
}
rootCAs, err := x509.SystemCertPool()
if rootCAs == nil || err != nil {
rootCAs = x509.NewCertPool()
@@ -215,9 +231,8 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
}
var keys []string
plaintext := app.config.Section("email").Key("plaintext").MustBool(false)
telegram := app.config.Section("telegram").Key("enabled").MustBool(false)
if plaintext {
if telegram {
if telegramEnabled || discordEnabled {
keys = []string{"text"}
text, markdown = "", ""
} else {
@@ -225,7 +240,7 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
text = ""
}
} else {
if telegram {
if telegramEnabled || discordEnabled {
keys = []string{"html", "text", "markdown"}
} else {
keys = []string{"html", "text"}
@@ -324,7 +339,6 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Message, error) {
email := &Message{Subject: subject}
renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
html := markdown.ToHTML([]byte(md), nil, renderer)
text := stripMarkdown(md)
message := app.config.Section("messages").Key("message").String()
@@ -522,7 +536,7 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo
if inviteLink != "" {
// Strip /invite form end of this URL, ik its ugly.
template["link_reset"] = true
pinLink := fmt.Sprintf("%s/reset?pin=%s", strings.Replace(inviteLink, "/invite", "", 1), pwr.Pin)
pinLink := fmt.Sprintf("%s/reset?pin=%s", strings.TrimPrefix(inviteLink, "/invite"), pwr.Pin)
template["pin"] = pinLink
// Only used in html email.
template["pin_code"] = pwr.Pin
@@ -794,8 +808,27 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
var err error
if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled {
err = app.telegram.Send(email, tgChat.ChatID)
} else if address, ok := app.storage.emails[id]; ok {
err = app.email.send(email, address.(string))
if err != nil {
return err
}
}
if dcChat, ok := app.storage.discord[id]; ok && dcChat.Contact && discordEnabled {
err = app.discord.Send(email, dcChat.ChannelID)
if err != nil {
return err
}
}
if mxChat, ok := app.storage.matrix[id]; ok && mxChat.Contact && matrixEnabled {
err = app.matrix.Send(email, mxChat.RoomID)
if err != nil {
return err
}
}
if address, ok := app.storage.emails[id]; ok && address.Contact && emailEnabled {
err = app.email.send(email, address.Addr)
if err != nil {
return err
}
}
if err != nil {
return err
@@ -805,11 +838,14 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
}
func (app *appContext) getAddressOrName(jfID string) string {
if dcChat, ok := app.storage.discord[jfID]; ok && dcChat.Contact && discordEnabled {
return dcChat.Username + "#" + dcChat.Discriminator
}
if tgChat, ok := app.storage.telegram[jfID]; ok && tgChat.Contact && telegramEnabled {
return "@" + tgChat.Username
}
if addr, ok := app.storage.emails[jfID]; ok {
return addr.(string)
return addr.Addr
}
return ""
}

44
exit.go Normal file
View File

@@ -0,0 +1,44 @@
package main
import (
"html/template"
"log"
"os"
"path/filepath"
"time"
"github.com/pkg/browser"
)
// Exit dumps the last 100 lines of output to a crash file in /tmp (or equivalent), and generates a prettier HTML file containing it that is opened in the browser if possible.
func Exit(err interface{}) {
tmpl, err2 := template.ParseFS(localFS, "html/crash.html", "html/header.html")
if err2 != nil {
log.Fatalf("Failed to load template: %v", err)
}
logCache := lineCache.String()
sanitized := sanitizeLog(logCache)
data := map[string]interface{}{
"Log": logCache,
"SanitizedLog": sanitized,
}
if err != nil {
data["Err"] = err
}
fpath := filepath.Join(temp, "jfa-go-crash-"+time.Now().Local().Format("2006-01-02T15:04:05"))
err2 = os.WriteFile(fpath+".txt", []byte(logCache), 0666)
if err2 != nil {
log.Fatalf("Failed to write crash dump file: %v", err2)
}
log.Printf("\n------\nA crash report has been saved to \"%s\".\n------", fpath+".txt")
f, err2 := os.OpenFile(fpath+".html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err2 != nil {
log.Fatalf("Failed to open crash dump file: %v", err2)
}
defer f.Close()
err2 = tmpl.Execute(f, data)
if err2 != nil {
log.Fatalf("Failed to execute template: %v", err2)
}
browser.OpenFile(fpath + ".html")
}

22
go.mod
View File

@@ -10,30 +10,42 @@ replace github.com/hrfee/jfa-go/ombi => ./ombi
replace github.com/hrfee/jfa-go/logger => ./logger
replace github.com/hrfee/jfa-go/linecache => ./linecache
require (
github.com/bwmarrin/discordgo v0.23.2
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/fatih/color v1.10.0
github.com/fsnotify/fsnotify v1.4.9
github.com/getlantern/systray v1.1.0
github.com/gin-contrib/pprof v1.3.0
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
github.com/gin-gonic/gin v1.6.3
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.3 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/golang/protobuf v1.4.3 // indirect
github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd
github.com/google/uuid v1.1.2 // indirect
github.com/hrfee/jfa-go/common v0.0.0-20210105184019-fdc97b4e86cc
github.com/hrfee/jfa-go/docs v0.0.0-20201112212552-b6f3cd7c1f71
github.com/hrfee/jfa-go/linecache v0.0.0-00010101000000-000000000000 // indirect
github.com/hrfee/jfa-go/logger v0.0.0-00010101000000-000000000000
github.com/hrfee/jfa-go/ombi v0.0.0-20201112212552-b6f3cd7c1f71
github.com/hrfee/mediabrowser v0.3.3
github.com/hrfee/mediabrowser v0.3.4
github.com/itchyny/timefmt-go v0.1.2
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/lithammer/shortuuid/v3 v3.0.4
github.com/mailgun/mailgun-go/v4 v4.5.1
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16
github.com/pkg/browser v0.0.0-20210606212950-a7b7a6107d32 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
github.com/swaggo/gin-swagger v1.3.0
@@ -42,9 +54,9 @@ require (
github.com/ugorji/go v1.2.0 // indirect
github.com/writeas/go-strip-markdown v2.0.1+incompatible
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c // indirect
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 // indirect
golang.org/x/tools v0.1.0 // indirect
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b // indirect
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 // indirect
golang.org/x/tools v0.1.3 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/ini.v1 v1.62.0
)

79
go.sum
View File

@@ -11,12 +11,16 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -26,6 +30,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
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/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
@@ -40,6 +46,20 @@ github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.1.0 h1:U0wCEqseLi2ok1fE6b88gJklzriavPJixZysZPkZd/Y=
github.com/getlantern/systray v1.1.0/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc=
@@ -70,6 +90,8 @@ github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL9
github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
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.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
@@ -93,6 +115,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
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/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
@@ -128,8 +152,10 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hrfee/mediabrowser v0.3.3 h1:7E05uiol8hh2ytKn3WVLrUIvHAyifYEIy3Y5qtuNh8I=
github.com/hrfee/mediabrowser v0.3.3/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hrfee/mediabrowser v0.3.4 h1:D2FTnuRDXUUAHW80L1kamhVUKNifm8peZVVPNe0yWmA=
github.com/hrfee/mediabrowser v0.3.4/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/itchyny/timefmt-go v0.1.2 h1:q0Xa4P5it6K6D7ISsbLAMwx1PnWlixDcJL6/sFs93Hs=
github.com/itchyny/timefmt-go v0.1.2/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
@@ -165,6 +191,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 h1:ZtO5uywdd5dLDCud4r0r55eP4j9FuUNpl60Gmntcop4=
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@@ -181,6 +209,10 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
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/pkg/browser v0.0.0-20210606212950-a7b7a6107d32 h1:K3WnH8Ka32vWygzmjKEhz1zAVqckNoWDqX3azMxuiSA=
github.com/pkg/browser v0.0.0-20210606212950-a7b7a6107d32/go.mod h1:yvwcBfzEX4m+eTgxPBbNYytaWFv4PSQzBaeYjxp8Iik=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -189,10 +221,14 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCb
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
@@ -235,10 +271,12 @@ github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead h1:jeP6FgaSLNTMP+Yri3qjlACywQLye+huGLmNGhBzm6k=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -251,8 +289,9 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -268,16 +307,21 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/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-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c h1:KHUzaHIpjWVlVVNh65G3hhuj3KB1HnjY6Cq5cTvRQT8=
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -289,20 +333,27 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210531080801-fdfd190a6549 h1:OL5GcZ2XPkte3dpfuFQ9o884vrE3BZQhajdntNMruv4=
golang.org/x/sys v0.0.0-20210531080801-fdfd190a6549/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 h1:faDu4veV+8pcThn4fewv6TVlNCezafGoC1gM/mxQLbQ=
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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=
@@ -314,8 +365,10 @@ golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3 h1:L69ShwSZEyCsLKoAxDKeMvLDZkumEe8gXUZAjab0tX8=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -5,8 +5,10 @@
<script>
window.URLBase = "{{ .urlBase }}";
window.notificationsEnabled = {{ .notifications }};
window.emailEnabled = {{ .email_enabled }};
window.telegramEnabled = {{ .telegram_enabled }};
window.emailEnabled = {{ .emailEnabled }};
window.telegramEnabled = {{ .telegramEnabled }};
window.discordEnabled = {{ .discordEnabled }};
window.matrixEnabled = {{ .matrixEnabled }};
window.ombiEnabled = {{ .ombiEnabled }};
window.usernameEnabled = {{ .username }};
window.langFile = JSON.parse({{ .language }});
@@ -309,7 +311,7 @@
<span class="button ~urge !normal full-width center" id="update-update">{{ .strings.update }}</span>
</div>
</div>
{{ if .telegram_enabled }}
{{ if .telegramEnabled }}
<div id="modal-telegram" class="modal">
<div class="modal-content card">
<span class="heading mb-1">{{ .strings.linkTelegram }}</span>
@@ -327,6 +329,31 @@
</div>
</div>
{{ end }}
{{ if .discordEnabled }}
<div id="modal-discord" class="modal">
<div class="modal-content card">
<span class="heading mb-1"><span id="discord-header"></span><span class="modal-close">&times;</span></span>
<p class="content mb-1" id="discord-description"></p>
<div class="row">
<input type="search" class="col sm field ~neutral !normal input" id="discord-search" placeholder="user#1234">
</div>
<table class="table"><tbody id="discord-list"></tbody></table>
</div>
</div>
{{ end }}
<div id="modal-matrix" class="modal">
<form class="modal-content card" id="form-matrix" href="">
<span class="heading">{{ .strings.linkMatrix }}</span>
<p class="content">{{ .strings.linkMatrixDescription }}</p>
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.matrixHomeServer }}" id="matrix-homeserver">
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.username }}" id="matrix-user">
<input type="password" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.password }}" id="matrix-password">
<label>
<input type="submit" class="unfocused">
<span class="button ~urge !normal full-width center supra submit">{{ .strings.submit }}</span>
</label>
</form>
</div>
<div id="notification-box"></div>
<span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
@@ -488,7 +515,14 @@
<div id="create-send-to-container">
<label class="label supra">{{ .strings.inviteSendToEmail }}</label>
<div class="flex-expand mb-1 mt-half">
{{ if .discordEnabled }}
<input type="text" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com | user#1234">
<span id="create-send-to-search" class="button ~neutral !normal mr-1">
<i class="icon ri-search-2-line" title="{{ .strings.search }}"></i>
</span>
{{ else }}
<input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com">
{{ end }}
<label for="create-send-to-enabled" class="button ~neutral !normal">
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
</label>
@@ -522,9 +556,15 @@
<th><input type="checkbox" value="" id="accounts-select-all"></th>
<th>{{ .strings.username }}</th>
<th>{{ .strings.emailAddress }}</th>
{{ if .telegram_enabled }}
{{ if .telegramEnabled }}
<th>Telegram</th>
{{ end }}
{{ if .matrixEnabled }}
<th>Matrix</th>
{{ end }}
{{ if .discordEnabled }}
<th>Discord</th>
{{ end }}
<th>{{ .strings.expiry }}</th>
<th>{{ .strings.lastActiveTime }}</th>
</tr>

49
html/crash.html Normal file
View File

@@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<link inline rel="stylesheet" type="text/css" href="bundle.css">
{{ template "header.html" . }}
<title>Crash report</title>
</head>
<body>
<div class="page-container">
<div class="row">
<div class="col">
<div class="card ~critical sectioned">
<section class="section ~critical">
<span class="heading">Crash report for jfa-go</span>
{{ if .Err }}
<div class="monospace pre-line mt-1 mb-1">
Error: {{ .Err }}
</div>
{{ end }}
<a class="button ~critical mb-1" 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-expand">
<span class="subheading">Full Log</span>
<span class="button ~urge ml-half" id="copy-log">Copy</span>
</div>
<div class="row mb-1">
<label class="col mr-1">
<span class="button ~neutral !high supra full-width center" id="button-log-normal">Normal</span>
</label>
<label class="col mr-1">
<span class="button ~neutral !normal supra full-width center" id="button-log-sanitized">Sanitized</span>
</label>
</div>
<div id="log-normal">
<pre class="monospace 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="monospace pre-line">{{ .SanitizedLog }}</pre>
</div>
</section>
</div>
</div>
</div>
</div>
<script inline src="crash.js"></script>
</body>
</html>

View File

@@ -17,6 +17,18 @@
window.telegramEnabled = {{ .telegramEnabled }};
window.telegramRequired = {{ .telegramRequired }};
window.telegramPIN = "{{ .telegramPIN }}";
window.discordEnabled = {{ .discordEnabled }};
window.discordRequired = {{ .discordRequired }};
window.discordPIN = "{{ .discordPIN }}";
window.discordInviteLink = {{ .discordInviteLink }};
window.discordServerName = "{{ .discordServerName }}";
window.matrixEnabled = {{ .matrixEnabled }};
window.matrixRequired = {{ .matrixRequired }};
window.matrixUserID = "{{ .matrixUser }}";
</script>
{{ if .passwordReset }}
<script src="js/pwr.js" type="module"></script>
{{ else }}
<script src="js/form.js" type="module"></script>
{{ end }}
{{ end }}

View File

@@ -3,7 +3,13 @@
<head>
<link rel="stylesheet" type="text/css" href="css/bundle.css">
{{ template "header.html" . }}
<title>{{ .strings.pageTitle }}</title>
<title>
{{ if .passwordReset }}
{{ .strings.passwordReset }}
{{ else }}
{{ .strings.pageTitle }}
{{ end }}
</title>
</head>
<body class="max-w-full overflow-x-hidden section">
<div id="modal-success" class="modal">
@@ -37,6 +43,35 @@
</div>
</div>
{{ end }}
{{ if .discordEnabled }}
<div id="modal-discord" class="modal">
<div class="modal-content card">
<span class="heading mb-1">{{ .strings.linkDiscord }}</span>
<p class="content mb-1"> {{ .discordSendPINMessage }}</p>
<h1 class="ac">{{ .discordPIN }}</h1>
<a id="discord-invite"></a>
<span class="button ~info !normal full-width center mt-1" id="discord-waiting">{{ .strings.success }}</span>
</div>
</div>
{{ end }}
{{ if .matrixEnabled }}
<div id="modal-matrix" class="modal">
<div class="modal-content card">
<span class="heading mb-1">{{ .strings.linkMatrix }}</span>
<p class="content mb-1"> {{ .strings.matrixEnterUser }}</p>
<input type="text" class="input ~neutral !high" placeholder="@user:riot.im" id="matrix-userid">
<div class="subheading link-center mt-1">
<span class="shield ~info mr-1">
<span class="icon">
<i class="ri-chat-3-line"></i>
</span>
</span>
{{ .matrixUser }}
</div>
<span class="button ~info !normal full-width center mt-1" id="matrix-send">{{ .strings.submit }}</span>
</div>
</div>
{{ end }}
<span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
<i class="ri-global-line"></i>
@@ -51,8 +86,20 @@
<div class="page-container">
<div class="card ~neutral !low">
<div class="row baseline">
<span class="col heading">{{ .strings.createAccountHeader }}</span>
<span class="col subheading"> {{ .helpMessage }}</span>
<span class="col heading">
{{ if .passwordReset }}
{{ .strings.passwordReset }}
{{ else }}
{{ .strings.createAccountHeader }}
{{ end }}
</span>
<span class="col subheading">
{{ if .passwordReset }}
{{ .strings.enterYourPassword }}
{{ else }}
{{ .helpMessage }}
{{ end }}
</span>
</div>
<div class="row">
<div class="col">
@@ -60,6 +107,7 @@
<aside class="col aside sm ~warning" id="user-expiry-message"></aside>
{{ end }}
<form class="card ~neutral !normal" id="form-create" href="">
{{ if not .passwordReset }}
<label class="label supra">
{{ .strings.username }}
<input type="text" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.username }}" id="create-username" aria-label="{{ .strings.username }}">
@@ -69,15 +117,36 @@
<input type="email" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.emailAddress }}" id="create-email" aria-label="{{ .strings.emailAddress }}" value="{{ .email }}">
{{ if .telegramEnabled }}
<span class="button ~info !normal full-width center mb-1" id="link-telegram">{{ .strings.linkTelegram }}</span>
{{ end }}
{{ if .discordEnabled }}
<span class="button ~info !normal full-width center mb-1" id="link-discord">{{ .strings.linkDiscord }}</span>
{{ end }}
{{ if .matrixEnabled }}
<span class="button ~info !normal full-width center mb-1" id="link-matrix">{{ .strings.linkMatrix }}</span>
{{ end }}
{{ if or (.telegramEnabled) (or .discordEnabled .matrixEnabled) }}
<div id="contact-via" class="unfocused">
<label class="row switch pb-1">
<input type="radio" name="contact-via" value="email"><span>Contact through Email</span>
</label>
{{ if .telegramEnabled }}
<label class="row switch pb-1">
<input type="radio" name="contact-via" value="telegram" id="contact-via-telegram"><span>Contact through Telegram</span>
</label>
{{ end }}
{{ if .discordEnabled }}
<label class="row switch pb-1">
<input type="radio" name="contact-via" value="discord" id="contact-via-discord"><span>Contact through Discord</span>
</label>
{{ end }}
{{ if .matrixEnabled }}
<label class="row switch pb-1">
<input type="radio" name="contact-via" value="matrix" id="contact-via-matrix"><span>Contact through Matrix</span>
</label>
{{ end }}
</div>
{{ end }}
{{ end }}
<label class="label supra" for="create-password">{{ .strings.password }}</label>
<input type="password" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.password }}" id="create-password" aria-label="{{ .strings.password }}">
@@ -85,7 +154,13 @@
<input type="password" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.password }}" id="create-reenter-password" aria-label="{{ .strings.reEnterPassword }}">
<label>
<input type="submit" class="unfocused">
<span class="button ~urge !normal full-width center supra submit">{{ .strings.createAccountButton }}</span>
<span class="button ~urge !normal full-width center supra submit">
{{ if .passwordReset }}
{{ .strings.reset }}
{{ else }}
{{ .strings.createAccountButton }}
{{ end }}
</span>
</label>
</form>
</div>
@@ -110,4 +185,3 @@
{{ template "form-base" . }}
</body>
</html>

View File

@@ -40,6 +40,6 @@
</div>
<i class="content">{{ .contactMessage }}</i>
</div>
<script src="{{ .urlBase }}/js/pwr.js" type="module"></script>
<script src="{{ .urlBase }}/js/pwr-pin.js" type="module"></script>
</body>
</html>

BIN
images/discord/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
images/discord/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
images/discord/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
images/discord/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
images/discord/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
images/discord/6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

BIN
images/discord/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
images/discord/8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
images/matrix/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

BIN
images/matrix/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
images/matrix/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
images/matrix/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
images/tg-settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
images/tg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -42,7 +42,7 @@ func (rt *inviteDaemon) run() {
}
}
func (rt *inviteDaemon) shutdown() {
func (rt *inviteDaemon) Shutdown() {
rt.Stopped = true
rt.ShutdownChannel <- "Down"
<-rt.ShutdownChannel

View File

@@ -32,7 +32,7 @@
"modifySettings": "Einstellungen ändern",
"modifySettingsDescription": "Wende Einstellungen von einem bestehenden Profil an, oder beziehe sie direkt von einem Benutzer.",
"applyHomescreenLayout": "Startbildschirmlayout anwenden",
"sendDeleteNotificationEmail": "Benachrichtigungs-E-Mail senden",
"sendDeleteNotificationEmail": "Benachrichtigung senden",
"sendDeleteNotifiationExample": "Dein Konto wurde gelöscht.",
"settingsRestartRequired": "Neustart erforderlich",
"settingsRestartRequiredDescription": "Ein Neustart ist notwendig, um einige Einstellungen anzuwenden, die du geändert hast. Jetzt oder später neu starten?",
@@ -72,7 +72,7 @@
"customizeMessages": "E-Mails anpassen",
"customizeMessagesDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
"announce": "Ankündigen",
"subject": "E-Mail-Betreff",
"subject": "Betreff",
"message": "Nachricht",
"markdownSupported": "Markdown wird unterstützt.",
"advancedSettings": "Erweiterte Einstellungen",
@@ -87,7 +87,20 @@
"update": "Aktualisieren",
"updates": "Aktualisierungen",
"expiry": "Ablaufdatum",
"extendExpiry": "Ablaufdatum verlängern"
"extendExpiry": "Ablaufdatum verlängern",
"reEnable": "Wieder aktivieren",
"disable": "Deaktivieren",
"donate": "Spenden",
"conditionals": "Bedingungen",
"contactThrough": "Kontakt über:",
"sendPIN": "Bitte den Benutzer, die unten stehende PIN an den Bot zu senden.",
"inviteMonths": "Monate",
"add": "Hinzufügen",
"select": "Auswählen",
"searchDiscordUser": "Gib den Discord-Benutzername ein, um den Benutzer zu finden.",
"findDiscordUser": "Suche Discord-Benutzer",
"linkMatrixDescription": "Gib den Benutzernamen und das Passwort des Benutzers ein, der als Bot verwendet werden soll. Nach dem Absenden wird die App neu gestartet.",
"matrixHomeServer": "Adresse des Homeservers"
},
"notifications": {
"changedEmailAddress": "E-Mail-Adresse von {n} geändert.",
@@ -116,7 +129,7 @@
"errorFailureCheckLogs": "Fehlgeschlagen (überprüfe die Konsole/Logs)",
"errorPartialFailureCheckLogs": "Teilweiser Fehlschlag (überprüfe die Konsole/Logs)",
"errorUserCreated": "Fehler beim Erstellen des Benutzers {n}.",
"errorSendWelcomeEmail": "Fehler beim Senden der Willkommens-E-Mail (überprüfe die Konsole/Logs)",
"errorSendWelcomeEmail": "Fehler beim Senden der Willkommensnachricht (überprüfe die Konsole/Logs)",
"saveEmail": "E-Mail gespeichert.",
"errorSaveEmail": "Fehler beim Speichern der E-Mail.",
"sentAnnouncement": "Ankündigung gesendet.",
@@ -124,7 +137,10 @@
"errorApplyUpdate": "Fehler beim Anwenden der Aktualisierung, versuche es manuell.",
"errorCheckUpdate": "Fehler beim Suchen nach Aktualisierungen.",
"updateAvailable": "Eine neue Aktualisierung ist verfügbar, überprüfe die Einstellungen.",
"noUpdatesAvailable": "Keinen neuen Aktualisierungen verfügbar."
"noUpdatesAvailable": "Keinen neuen Aktualisierungen verfügbar.",
"updateAppliedRefresh": "Update angewendet, bitte aktualisieren.",
"telegramVerified": "Telegram-Konto verifiziert.",
"accountConnected": "Konto verbunden."
},
"quantityStrings": {
"modifySettingsFor": {
@@ -162,6 +178,22 @@
"extendedExpiry": {
"singular": "Ablaufdatum für {n} Benutzer verlängern.",
"plural": "Ablaufdatum für {n} Benutzer verlängern."
},
"disabledUser": {
"plural": "Benutzer {n} Deaktiviert.",
"singular": "Benutzer {n} Deaktiviert."
},
"enabledUser": {
"singular": "Benutzer {n} Aktiviert.",
"plural": "Benutzer {n} Aktiviert."
},
"disableUsers": {
"singular": "Benutzer {n} deaktivieren",
"plural": "Deaktiviere {n} Benutzer"
},
"reEnableUsers": {
"singular": "Benutzer {n} wieder aktivieren",
"plural": "Benutzer {n} wieder aktivieren"
}
}
}

View File

@@ -20,6 +20,8 @@
"create": "Create",
"apply": "Apply",
"delete": "Delete",
"add": "Add",
"select": "Select",
"name": "Name",
"date": "Date",
"enabled": "Enabled",
@@ -94,7 +96,11 @@
"notifyEvent": "Notify on:",
"notifyInviteExpiry": "On expiry",
"notifyUserCreation": "On user creation",
"sendPIN": "Ask the user to send the PIN below to the bot."
"sendPIN": "Ask the user to send the PIN below to the bot.",
"searchDiscordUser": "Start typing the Discord username to find the user.",
"findDiscordUser": "Find Discord user",
"linkMatrixDescription": "Enter the username and password of the user to use as a bot. Once submitted, the app will restart.",
"matrixHomeServer": "Home server address"
},
"notifications": {
"changedEmailAddress": "Changed email address of {n}.",
@@ -107,6 +113,7 @@
"updateApplied": "Update applied, please restart.",
"updateAppliedRefresh": "Update applied, please refresh.",
"telegramVerified": "Telegram account verified.",
"accountConnected": "Account connected.",
"errorConnection": "Couldn't connect to jfa-go.",
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",

View File

@@ -91,7 +91,8 @@
"notifyEvent": "Notificar en:",
"notifyInviteExpiry": "Al vencimiento",
"notifyUserCreation": "Sobre la creación de usuarios",
"conditionals": "Condicionales"
"conditionals": "Condicionales",
"donate": "Donar"
},
"notifications": {
"changedEmailAddress": "Se cambió la dirección de correo electrónico de {n}.",

View File

@@ -34,7 +34,7 @@
"modifySettings": "Modifier les paramètres",
"modifySettingsDescription": "Appliquez les paramètres à partir d'un profil existant ou obtenez-les directement auprès d'un utilisateur.",
"applyHomescreenLayout": "Appliquer la disposition de l'écran d'accueil",
"sendDeleteNotificationEmail": "Envoyer un e-mail de notification",
"sendDeleteNotificationEmail": "Envoyer un message de notification",
"sendDeleteNotifiationExample": "Votre compte a été supprimé.",
"settingsRestartRequired": "Redémarrage nécessaire",
"settingsRestartRequiredDescription": "Un redémarrage est nécessaire pour appliquer certains paramètres que vous avez modifiés. Redémarrer maintenant ou plus tard ?",
@@ -90,7 +90,12 @@
"update": "Mise à jour",
"download": "Téléchargement",
"search": "Recherche",
"conditionals": "Conditions"
"conditionals": "Conditions",
"userExpiryDescription": "Un laps de temps spécifié après chaque inscription, jfa-go supprimera / désactivera le compte. Vous pouvez modifier ce comportement dans les paramètres.",
"donate": "Faire un don",
"extendExpiry": "Prolonger l'expiration",
"contactThrough": "Contactez par :",
"sendPIN": "Demandez à l'utilisateur d'envoyer le code PIN ci-dessous au bot."
},
"notifications": {
"changedEmailAddress": "Adresse e-mail modifiée de {n}.",
@@ -119,11 +124,17 @@
"errorFailureCheckLogs": "Échec (vérifier la console / les journaux)",
"errorPartialFailureCheckLogs": "Panne partielle (vérifier la console / les journaux)",
"errorUserCreated": "Echec lors de la création de l'utilisateur {n}.",
"errorSendWelcomeEmail": "Echec lors de l'envoi du mail de bienvenue (vérifier la console/les journaux)",
"errorSendWelcomeEmail": "Echec lors de l'envoi du message de bienvenue (vérifier la console/les journaux)",
"sentAnnouncement": "Annonce envoyée.",
"saveEmail": "Email enregistré.",
"errorSaveEmail": "Échec de l'enregistrement de l'e-mail.",
"updateApplied": "Mise à jour appliquée, veuillez redémarrer."
"updateApplied": "Mise à jour appliquée, veuillez redémarrer.",
"errorApplyUpdate": "Échec de l'application de la mise à jour, essayez manuellement.",
"errorCheckUpdate": "Échec de la vérification de la mise à jour.",
"updateAvailable": "Une nouvelle mise à jour est disponible, vérifiez les paramètres.",
"noUpdatesAvailable": "Aucune nouvelle mise à jour disponible.",
"telegramVerified": "Compte Telegram vérifié.",
"updateAppliedRefresh": "Mise à jour appliquée, veuillez actualiser."
},
"quantityStrings": {
"modifySettingsFor": {
@@ -153,6 +164,30 @@
"announceTo": {
"singular": "Annonce à {n} utilisateur",
"plural": "Annonce à {n} utilisateurs"
},
"enabledUser": {
"plural": "{n} utilisateurs activés.",
"singular": "{n} utilisateur activé."
},
"extendExpiry": {
"singular": "Prolonger l'expiration pour {n} utilisateur",
"plural": "Prolonger l'expiration pour {n} utilisateurs"
},
"extendedExpiry": {
"singular": "Expiration prolongée pour {n} utilisateur.",
"plural": "Expiration prolongée pour {n} utilisateurs."
},
"disableUsers": {
"singular": "Désactiver {n} utilisateur",
"plural": "Désactiver {n} utilisateurs"
},
"reEnableUsers": {
"singular": "Ré-activer {n} utilisateur",
"plural": "Ré-activer {n} utilisateurs"
},
"disabledUser": {
"singular": "{n} utilisateur désactivé.",
"plural": "{n} utilisateurs désactivés."
}
}
}

View File

@@ -32,7 +32,7 @@
"modifySettings": "Instellingen aanpassen",
"modifySettingsDescription": "Pas instellingen van een bestaand profiel toe, of neem ze direct over van een gebruiker.",
"applyHomescreenLayout": "Sla startpagina indeling op",
"sendDeleteNotificationEmail": "Stuur meldingse-mail",
"sendDeleteNotificationEmail": "Stuur melding",
"sendDeleteNotifiationExample": "Je account is verwijderd.",
"settingsRestartRequired": "Herstart nodig",
"settingsRestartRequiredDescription": "Er is een herstart nodig om de wijzigingen door te voeren. Herstart nu of later?",
@@ -67,7 +67,7 @@
"settingsRestarting": "Aan het herstarten…",
"announce": "Aankondiging",
"markdownSupported": "Markdown wordt ondersteund.",
"subject": "E-mailonderwerp",
"subject": "Onderwerp",
"message": "Bericht",
"variables": "Variabelen",
"customizeMessagesDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
@@ -91,7 +91,16 @@
"inviteMonths": "Maanden",
"reEnable": "Opnieuw inschakelen",
"disable": "Uitschakelen",
"conditionals": "Voorwaarden"
"conditionals": "Voorwaarden",
"donate": "Doneer",
"contactThrough": "Stuur bericht via:",
"sendPIN": "Vraag de gebruiker om onderstaande pincode naar de bot te sturen.",
"add": "Voeg toe",
"searchDiscordUser": "Begin de Discord gebruikersnaam te typen om de gebruiker te vinden.",
"linkMatrixDescription": "Vul de gebruikersnaam en wachtwoord in van de gebruiker om als bot te gebruiken. De app start zodra ze zijn verstuurd.",
"select": "Selecteer",
"findDiscordUser": "Zoek Discord gebruiker",
"matrixHomeServer": "Adres home server"
},
"notifications": {
"changedEmailAddress": "E-mailadres van {n} gewijzigd.",
@@ -119,7 +128,7 @@
"errorChangedEmailAddress": "Wijzigen van e-mailadres van {n} mislukt.",
"errorFailureCheckLogs": "Mislukt (controleer console/logbestanden)",
"errorPartialFailureCheckLogs": "Gedeeltelijke fout (controleer console/logbestanden)",
"errorSendWelcomeEmail": "Versturen van welkomste-mail is mislukt (zie console/logs)",
"errorSendWelcomeEmail": "Versturen van welkomstbericht is mislukt (zie console/logs)",
"errorUserCreated": "Aanmaken van gebruiker {n} is mislukt.",
"sentAnnouncement": "Aankondiging verzonden.",
"saveEmail": "E-mail opgeslagen.",
@@ -128,7 +137,10 @@
"errorApplyUpdate": "Installatie van update mislukt, probeer handmatig.",
"errorCheckUpdate": "Controleren op update mislukt.",
"updateAvailable": "Er is een nieuwe update beschikbaar, kijk bij instellingen.",
"noUpdatesAvailable": "Geen nieuwe updates beschikbaar."
"noUpdatesAvailable": "Geen nieuwe updates beschikbaar.",
"telegramVerified": "Telegram-account goedgekeurd.",
"updateAppliedRefresh": "Update toegepast, ververs alsjeblieft.",
"accountConnected": "Account gekoppeld."
},
"quantityStrings": {
"modifySettingsFor": {

View File

@@ -33,7 +33,7 @@
"modifySettings": "Modificar configurações",
"modifySettingsDescription": "Aplique as configurações de um perfil existente ou obtenha-as diretamente de um usuário.",
"applyHomescreenLayout": "Aplicar layout na tela inicial",
"sendDeleteNotificationEmail": "Enviar email de notificação",
"sendDeleteNotificationEmail": "Enviar mensagem de notificação",
"sendDeleteNotifiationExample": "Sua conta foi deletada.",
"settingsRestartRequired": "Necessário reiniciar",
"settingsRestartRequiredDescription": "É necessário reiniciar para aplicar algumas configurações alteradas. Deseja reiniciar agora ou mais tarde?",
@@ -66,7 +66,7 @@
"settingsRestart": "Reiniciar",
"settingsRestarting": "Reiniciando…",
"announce": "Anunciar",
"subject": "Assunto do email",
"subject": "Assunto",
"message": "Mensagem",
"markdownSupported": "Suporte a Markdown.",
"customizeMessagesDescription": "Se não quiser usar os modelos de email do jfa-go, você pode criar o seu próprio usando o Markdown.",
@@ -91,7 +91,15 @@
"inviteMonths": "Meses",
"reEnable": "Reativar",
"disable": "Desativar",
"conditionals": "Condicionais"
"conditionals": "Condicionais",
"donate": "Doar",
"contactThrough": "Contato através:",
"sendPIN": "Peça ao usuário para enviar o PIN abaixo para o bot.",
"searchDiscordUser": "Digite o nome de usuário do Discord.",
"findDiscordUser": "Encontrar usuário Discord",
"add": "Adicionar",
"linkMatrixDescription": "Digite o nome de usuário e a senha para usar como bot. Depois de enviado, o aplicativo será reiniciado.",
"select": "Selecionar"
},
"notifications": {
"changedEmailAddress": "Endereço de e-mail alterado de {n}.",
@@ -120,7 +128,7 @@
"errorFailureCheckLogs": "Falha (verificar console/logs)",
"errorPartialFailureCheckLogs": "Falha parcial (verificar console/logs)",
"errorUserCreated": "Falha ao criar o usuário {n}.",
"errorSendWelcomeEmail": "Falha ao enviar e-mail de boas-vindas (verifique console/logs)",
"errorSendWelcomeEmail": "Falha ao enviar mensagem de boas-vindas (verifique console/logs)",
"sentAnnouncement": "Comunicado enviado.",
"saveEmail": "Email salvo.",
"errorSaveEmail": "Falha ao salvar o email.",
@@ -128,7 +136,10 @@
"errorApplyUpdate": "Falha ao aplicar a atualização, tente manualmente.",
"updateAvailable": "Uma nova atualização está disponível, verifique as configurações.",
"errorCheckUpdate": "Falha ao verificar atualizações.",
"noUpdatesAvailable": "Nenhuma atualização disponível."
"noUpdatesAvailable": "Nenhuma atualização disponível.",
"telegramVerified": "Conta do Telegram verificada.",
"updateAppliedRefresh": "Atualização instalada, atualize.",
"accountConnected": "Conta conectada."
},
"quantityStrings": {
"modifySettingsFor": {

View File

@@ -14,6 +14,13 @@
"theme": "Thema",
"time24h": "24h-Format",
"time12h": "12h-Format",
"copied": "Kopiert"
"copied": "Kopiert",
"linkTelegram": "Link Telegram",
"contactEmail": "Kontakt über E-Mail",
"contactTelegram": "Kontakt über Telegram",
"linkDiscord": "Link Discord",
"linkMatrix": "Link Matrix",
"send": "Senden",
"contactDiscord": "Kontakt über Discord"
}
}

View File

@@ -8,6 +8,7 @@
"emailAddress": "Email Address",
"name": "Name",
"submit": "Submit",
"send": "Send",
"success": "Success",
"error": "Error",
"copy": "Copy",
@@ -17,6 +18,9 @@
"linkTelegram": "Link Telegram",
"contactEmail": "Contact through Email",
"contactTelegram": "Contact through Telegram",
"linkDiscord": "Link Discord",
"linkMatrix": "Link Matrix",
"contactDiscord": "Contact through Discord",
"theme": "Theme"
}
}

View File

@@ -14,6 +14,10 @@
"copy": "Copier",
"time24h": "Temps 24h",
"time12h": "Temps 12h",
"theme": "Thème"
"theme": "Thème",
"copied": "Copié",
"linkTelegram": "Lien Telegram",
"contactEmail": "Contact par e-mail",
"contactTelegram": "Contact par Telegram"
}
}

View File

@@ -8,12 +8,19 @@
"password": "Wachtwoord",
"emailAddress": "E-mailadres",
"submit": "Verstuur",
"success": "Success",
"success": "Succes",
"error": "Fout",
"copy": "Kopiëer",
"theme": "Thema",
"time24h": "24u-formaat",
"time12h": "12u-formaat",
"copied": "Gekopieerd"
"copied": "Gekopieerd",
"linkTelegram": "Koppel Telegram",
"contactEmail": "Stuur e-mailbericht",
"contactTelegram": "Stuur Telegram-bericht",
"send": "Verstuur",
"linkDiscord": "Koppel Discord",
"linkMatrix": "Koppel Matrix",
"contactDiscord": "Stuur Discord bericht"
}
}

View File

@@ -14,6 +14,13 @@
"theme": "Tema",
"time24h": "Horário 24h",
"time12h": "Horário 12h",
"copied": "Copiado"
"copied": "Copiado",
"linkTelegram": "Link do Telegram",
"contactEmail": "Contato por Email",
"contactTelegram": "Contato pelo Telegram",
"send": "Enviar",
"linkDiscord": "Link do Discord",
"linkMatrix": "Link do Matrix",
"contactDiscord": "Contato através do Discord"
}
}

View File

@@ -3,7 +3,7 @@
"name": "Deutsch (DE)"
},
"strings": {
"ifItWasNotYou": "Wenn du das nicht warst, ignoriere bitte diese E-Mail.",
"ifItWasNotYou": "Wenn du das nicht warst, ignoriere bitte dies.",
"reason": "Grund",
"helloUser": "Hallo {username},"
},
@@ -27,7 +27,8 @@
"ifItWasYou": "Wenn du das warst, gib die PIN unten in die Eingabeaufforderung ein.",
"codeExpiry": "Der Code wird am {date}, um {time} UTC ablaufen, was in {expiresInMinutes} ist.",
"pin": "PIN",
"name": "Passwortzurücksetzung"
"name": "Passwortzurücksetzung",
"ifItWasYouLink": "Wenn du das warst, klick auf den Link unten."
},
"userDeleted": {
"title": "Dein Konto wurde gelöscht - Jellyfin",
@@ -48,7 +49,8 @@
"welcome": "Willkommen bei Jellyfin!",
"youCanLoginWith": "Du kannst dich mit den mit den untenstehenden Zugangsdaten anmelden",
"jellyfinURL": "URL",
"name": "Willkommens-E-Mail"
"name": "Willkommen",
"yourAccountWillExpire": "Dein Konto läuft am {date} ab."
},
"emailConfirmation": {
"title": "Bestätige deine E-Mail - Jellyfin",
@@ -61,5 +63,15 @@
"title": "Dein Konto ist abgelaufen - Jellyfin",
"yourAccountHasExpired": "Dein Konto ist abgelaufen.",
"contactTheAdmin": "Kontaktiere den Administrator für weitere Informationen."
},
"userDisabled": {
"name": "Benutzer deaktiviert",
"title": "Dein Konto wurde deaktiviert - Jellyfin",
"yourAccountWasDisabled": "Dein Konto wurde deaktiviert."
},
"userEnabled": {
"name": "Benutzer aktiviert",
"title": "Dein Konto wurde wieder freigeschaltet - Jellyfin",
"yourAccountWasEnabled": "Dein Konto wurde wieder aktiviert."
}
}

View File

@@ -4,7 +4,7 @@
"author": "https://github.com/Cornichon420"
},
"strings": {
"ifItWasNotYou": "Si ce n'était pas toi, tu peux ignorer ce mail.",
"ifItWasNotYou": "Si ce n'était pas toi, tu peux ignorer ceci.",
"reason": "Motif",
"helloUser": "Salut {username},"
},
@@ -50,7 +50,7 @@
"title": "Bienvenue sur Jellyfin",
"welcome": "Bienvenue sur Jellyfin !",
"jellyfinURL": "URL",
"name": "Courriel de bienvenue",
"name": "Bienvenue",
"yourAccountWillExpire": "Ton compte expirera le {date}."
},
"emailConfirmation": {

View File

@@ -3,7 +3,7 @@
"name": "Nederlands (NL)"
},
"strings": {
"ifItWasNotYou": "Als jij dit niet was, negeer dan alsjeblieft deze email.",
"ifItWasNotYou": "Als jij dit niet was, negeer dit dan alsjeblieft.",
"reason": "Reden",
"helloUser": "Hoi {username},"
},
@@ -49,7 +49,7 @@
"welcome": "Welkom bij Jellyfin!",
"youCanLoginWith": "Je kunt inloggen met onderstaande gegevens",
"jellyfinURL": "URL",
"name": "Welkomste-mail",
"name": "Welkom",
"yourAccountWillExpire": "Je account verloopt op {date}."
},
"emailConfirmation": {

View File

@@ -3,7 +3,7 @@
"name": "Português (BR)"
},
"strings": {
"ifItWasNotYou": "Se não foi você, ignore este e-mail.",
"ifItWasNotYou": "Se não foi você, ignore.",
"reason": "Razão",
"helloUser": "Ola {username},"
},
@@ -49,7 +49,7 @@
"welcome": "Bem vindo ao Jellyfin!",
"youCanLoginWith": "Abaixo está os detalhes para fazer o login",
"jellyfinURL": "URL",
"name": "Email de Boas vindas",
"name": "Bem-vindo",
"yourAccountWillExpire": "Sua conta irá expirar em {date}."
},
"emailConfirmation": {

View File

@@ -17,7 +17,10 @@
"successContinueButton": "Weiter",
"confirmationRequired": "E-Mail-Bestätigung erforderlich",
"confirmationRequiredMessage": "Bitte überprüfe dein Posteingang und bestätige deine E-Mail-Adresse.",
"yourAccountIsValidUntil": "Dein Konto wird bis zum {date} gültig sein."
"yourAccountIsValidUntil": "Dein Konto wird bis zum {date} gültig sein.",
"sendPIN": "Sende die untenstehende PIN an den Bot und komm dann hierher zurück, um dein Konto zu verbinden.",
"sendPINDiscord": "Gib auf Discord {command} in {server_channel} ein und sende die untenstehende PIN als DM an den Bot.",
"matrixEnterUser": "Gib deine Benutzer-ID ein und drücke auf Absenden. Anschließend erhälst du ein PIN, die hier eingegeben wird um fortzufahren."
},
"validationStrings": {
"length": {
@@ -43,6 +46,13 @@
},
"notifications": {
"errorUserExists": "Benutzer existiert bereits.",
"errorInvalidCode": "Ungültiger Invite-Code."
"errorInvalidCode": "Ungültiger Invite-Code.",
"telegramVerified": "Telegram-Konto verifiziert.",
"errorTelegramVerification": "Verifizierung von Telegram erforderlich.",
"errorInvalidPIN": "PIN ist ungültig.",
"errorDiscordVerification": "Discord-Verifizierung erforderlich.",
"errorMatrixVerification": "Matrix-Verifizierung erforderlich.",
"errorUnknown": "Unbekannter Fehler.",
"verified": "Konto verifiziert."
}
}

View File

@@ -18,14 +18,19 @@
"confirmationRequired": "Email confirmation required",
"confirmationRequiredMessage": "Please check your email inbox to verify your address.",
"yourAccountIsValidUntil": "Your account will be valid until {date}.",
"sendPIN": "Send the PIN below to the bot, then come back here to link your account."
"sendPIN": "Send the PIN below to the bot, then come back here to link your account.",
"sendPINDiscord": "Type {command} in {server_channel} on Discord, then send the PIN below via DM to the bot.",
"matrixEnterUser": "Enter your User ID, press submit, and a PIN will be sent to you. Enter it here to continue."
},
"notifications": {
"errorUserExists": "User already exists.",
"errorInvalidCode": "Invalid invite code.",
"errorTelegramVerification": "Telegram verification required.",
"errorInvalidPIN": "Telegram PIN is invalid.",
"telegramVerified": "Telegram account verified."
"errorDiscordVerification": "Discord verification required.",
"errorMatrixVerification": "Matrix verification required.",
"errorInvalidPIN": "PIN is invalid.",
"errorUnknown": "Unknown error.",
"verified": "Account verified."
},
"validationStrings": {
"length": {

View File

@@ -17,7 +17,9 @@
"successHeader": "Succes!",
"successContinueButton": "Continuer",
"confirmationRequired": "Confirmation de l'adresse e-mail requise",
"confirmationRequiredMessage": "Veuillez vérifier votre boite de réception pour confirmer votre adresse e-mail."
"confirmationRequiredMessage": "Veuillez vérifier votre boite de réception pour confirmer votre adresse e-mail.",
"yourAccountIsValidUntil": "Votre compte sera valide jusqu'au {date}.",
"sendPIN": "Envoyez le code PIN ci-dessous au bot, puis revenez ici pour lier votre compte."
},
"validationStrings": {
"length": {
@@ -43,6 +45,9 @@
},
"notifications": {
"errorUserExists": "Utilisateur déjà existant.",
"errorInvalidCode": "Code dinvitation non valide."
"errorInvalidCode": "Code dinvitation non valide.",
"errorTelegramVerification": "Vérification Telegram requise.",
"errorInvalidPIN": "PIN Telegram invalide.",
"telegramVerified": "Compte Telegram vérifié."
}
}

View File

@@ -17,7 +17,10 @@
"successContinueButton": "Doorgaan",
"confirmationRequired": "Bevestiging van e-mailadres verplicht",
"confirmationRequiredMessage": "Controleer je e-mail inbox om je adres te bevestigen.",
"yourAccountIsValidUntil": "Je account zal geldig zijn tot {date}."
"yourAccountIsValidUntil": "Je account zal geldig zijn tot {date}.",
"sendPIN": "Stuur onderstaande pincode naar de bot, en kom daarna hier terug om je account te koppelen.",
"matrixEnterUser": "Voer je gebruikers ID in, druk op versturen, en er wordt je een pincode toegestuurd. Vul die hier in om door te gaan.",
"sendPINDiscord": "Typ {command} in {server_channel} op Discord, stuur daarna onderstaande pincode via DM naar de bot."
},
"validationStrings": {
"length": {
@@ -43,6 +46,13 @@
},
"notifications": {
"errorUserExists": "Gebruiker bestaat al.",
"errorInvalidCode": "Ongeldige uitnodigingscode."
"errorInvalidCode": "Ongeldige uitnodigingscode.",
"telegramVerified": "Telegram-account goedgekeurd.",
"errorTelegramVerification": "Telegram-verificatie nodig.",
"errorInvalidPIN": "Pincode is ongeldig.",
"errorDiscordVerification": "Discord-verificatie vereist.",
"errorUnknown": "Onbekende fout.",
"errorMatrixVerification": "Matrix-verificatie vereist.",
"verified": "Account geverifieerd."
}
}

View File

@@ -15,13 +15,23 @@
"passwordRequirementsHeader": "Requisitos da Senha",
"successHeader": "Sucesso!",
"successContinueButton": "Continuar",
"confirmationRequired": "Necessária confirmação de e-mail",
"confirmationRequired": "Confirmação por e-mail",
"confirmationRequiredMessage": "Verifique sua caixa de email para finalizar o cadastro.",
"yourAccountIsValidUntil": "Sua conta é válida até {date}."
"yourAccountIsValidUntil": "Sua conta é válida até {date}.",
"sendPIN": "Envie o PIN abaixo para o bot e volte aqui para vincular sua conta.",
"sendPINDiscord": "Digite {command} em {server_channel} no Discord e envie o PIN abaixo via DM para o bot.",
"matrixEnterUser": "Digite sua ID de usuário, pressione enviar e um PIN será enviado. E digite aqui para continuar."
},
"notifications": {
"errorUserExists": "Esse usuário já existe.",
"errorInvalidCode": "Código do convite invalido."
"errorInvalidCode": "Código do convite invalido.",
"telegramVerified": "Conta do Telegram verificada.",
"errorInvalidPIN": "PIN inválido.",
"errorTelegramVerification": "Requer a verificação do telegram.",
"errorDiscordVerification": "Necessária verificação pelo Discord.",
"errorMatrixVerification": "Necessária verificação Matrix.",
"errorUnknown": "Erro desconhecido.",
"verified": "Conta verificada."
},
"validationStrings": {
"length": {

View File

@@ -17,11 +17,15 @@
"successContinueButton": "Fortsätt",
"confirmationRequired": "E-postbekräftelse krävs",
"confirmationRequiredMessage": "Kontrollera din e-postkorg för att verifiera din adress.",
"yourAccountIsValidUntil": "Ditt konto är giltigt fram tills {date}."
"yourAccountIsValidUntil": "Ditt konto är giltigt fram tills {date}.",
"sendPIN": "Skicka PIN-koden nedan till botten, återvänd sedan till den här sidan för att länka ditt konto."
},
"notifications": {
"errorUserExists": "Användare finns redan.",
"errorInvalidCode": "Ogiltig inbjudningskod."
"errorInvalidCode": "Ogiltig inbjudningskod.",
"errorInvalidPIN": "Telegram-PIN är ogiltig.",
"errorTelegramVerification": "Telegram-verifiering krävs.",
"telegramVerified": "Telegram-konto verifierades."
},
"validationStrings": {
"length": {

13
lang/pwreset/de-DE.json Normal file
View File

@@ -0,0 +1,13 @@
{
"meta": {
"name": "Deutsch (DE)"
},
"strings": {
"passwordReset": "Passwort zurücksetzen",
"resetFailed": "Zurücksetzen des Passworts fehlgeschlagen",
"tryAgain": "Bitte versuche es erneut.",
"youCanLogin": "Du kannst dich nun mit dem unten stehenden Code als Passwort anmelden.",
"youCanLoginOmbi": "Du kannst dich jetzt bei Jellyfin und Ombi mit dem unten stehenden Code als Passwort anmelden.",
"changeYourPassword": "Achte darauf, dass du dein Passwort nach der Anmeldung änderst."
}
}

13
lang/pwreset/en-GB.json Normal file
View File

@@ -0,0 +1,13 @@
{
"meta": {
"name": ""
},
"strings": {
"passwordReset": "",
"resetFailed": "",
"tryAgain": "",
"youCanLogin": "",
"youCanLoginOmbi": "",
"changeYourPassword": ""
}
}

View File

@@ -4,10 +4,12 @@
},
"strings": {
"passwordReset": "Password reset",
"reset": "Reset",
"resetFailed": "Password reset failed",
"tryAgain": "Please try again.",
"youCanLogin": "You can now log in with the below code as your password.",
"youCanLoginOmbi": "You can now log in to Jellyfin & Ombi with the below code as your password.",
"changeYourPassword": "Make sure to change your password after you log in."
"changeYourPassword": "Make sure to change your password after you log in.",
"enterYourPassword": "Enter your new password below."
}
}

View File

@@ -7,6 +7,7 @@
"resetFailed": "Error al cambiar contraseña",
"tryAgain": "Por favor intente nuevamente.",
"youCanLogin": "Ahora puedes logearte con el codigo como contraseña.",
"changeYourPassword": "Recuerda cambiar tu contraseña luego de iniciar sesión."
"changeYourPassword": "Recuerda cambiar tu contraseña luego de iniciar sesión.",
"youCanLoginOmbi": "Ahora puede iniciar sesión en Jellyfin & Ombi con el siguiente código como contraseña."
}
}

13
lang/pwreset/pt-BR.json Normal file
View File

@@ -0,0 +1,13 @@
{
"meta": {
"name": "Português (BR)"
},
"strings": {
"passwordReset": "Redefinir senha",
"resetFailed": "Falha ao redefinir a senha",
"tryAgain": "Tente novamente.",
"youCanLogin": "Agora você pode fazer login com o código abaixo como senha.",
"youCanLoginOmbi": "Agora você pode fazer login no Jellyfin & Ombi com o código abaixo como senha.",
"changeYourPassword": "Certifique-se de alterar sua senha depois de fazer o login."
}
}

View File

@@ -101,7 +101,10 @@
"title": "Passwortzurücksetzungen",
"description": "Wenn ein Benutzer versucht sein Passwort zurückzusetzen, erstellt Jellyfin eine Datei namens 'passwordreset-*.json', welche eine PIN enthält. jfa-go liest die Datei und sendet die PIN an den Benutzer.",
"pathToJellyfin": "Pfad zum Jellyfin-Konfigurationsverzeichnis",
"pathToJellyfinNotice": "Wenn du nicht weißt, wo das ist, versuche dein Passwort in Jellyfin zurückzusetzen. Ein Popup mit '<Pfad zu Jellyfin>/passwordreset-*.json' wird angezeigt werden."
"pathToJellyfinNotice": "Wenn du nicht weißt, wo das ist, versuche dein Passwort in Jellyfin zurückzusetzen. Ein Popup mit '<Pfad zu Jellyfin>/passwordreset-*.json' wird angezeigt werden.",
"resetLinks": "Einen Link statt einer PIN senden",
"resetLinksNotice": "Wenn die Ombi-Integration aktiviert ist, verwende dies, um zurückgesetzte Passwörter von Jellyfin mit Ombi zu synchronisieren.",
"resetLinksLanguage": "Standardsprache für den Link zum Zurücksetzen des Passworts"
},
"passwordValidation": {
"title": "Passwortüberprüfung",

View File

@@ -108,7 +108,10 @@
"title": "Restablecimiento de contraseña",
"description": "Cuando un usuario intenta restablecer su contraseña, Jellyfin crea un archivo llamado 'passwordreset - *. Json' que contiene un PIN. jfa-go lee el archivo y envía el PIN al usuario.",
"pathToJellyfin": "Ruta al directorio de configuración de Jellyfin",
"pathToJellyfinNotice": "Si no sabe dónde está, intente restablecer su contraseña en Jellyfin. Aparecerá una ventana emergente con '<ruta a jellyfin>/passwordreset-. Json'."
"pathToJellyfinNotice": "Si no sabe dónde está, intente restablecer su contraseña en Jellyfin. Aparecerá una ventana emergente con '<ruta a jellyfin>/passwordreset-. Json'.",
"resetLinks": "Envía un enlace en lugar de un PIN",
"resetLinksNotice": "Si la integración de Ombi está habilitada, utilícela para sincronizar los restablecimientos de contraseña de Jellyfin con Ombi.",
"resetLinksLanguage": "Enlace de restablecimiento predeterminado"
},
"passwordValidation": {
"title": "Validación de contraseña",

View File

@@ -101,7 +101,10 @@
"title": "Réinitialisation de mot de passe",
"description": "Lorsqu'un utilisateur essaie de réinitialiser son mot de passe, Jellyfin créé un fichier nommé 'passwordreset-*.json' qui contient le code PIN. jfa-go lit le fichier et envoie le code PIN à l'utilisateur.",
"pathToJellyfin": "Chemin du dossier de configuration de Jellyfin",
"pathToJellyfinNotice": "Si vous ne savez pas où c'est, essayez de réinitialiser votre mot de passe dans Jellyfin. Une popup avec '<path to jellyfin>/passwordreset-*.json' apparaitra."
"pathToJellyfinNotice": "Si vous ne savez pas où c'est, essayez de réinitialiser votre mot de passe dans Jellyfin. Une popup avec '<path to jellyfin>/passwordreset-*.json' apparaitra.",
"resetLinks": "Envoyer un lien plutôt qu'un PIN",
"resetLinksNotice": "Si l'intégration est activée, utilisez ceci pour synchroniser les réinitialisations de mots de passe Jellyfin avec Ombi.",
"resetLinksLanguage": "Langue du lien de réinitialisation par défaut"
},
"passwordValidation": {
"title": "Validation du mot de passe",
@@ -123,5 +126,12 @@
"successMessageNotice": "S'affiche lorsqu'un utilisateur crée son compte.",
"emailMessage": "Message de l'e-mail",
"emailMessageNotice": "S'affiche au bas des e-mails."
},
"updates": {
"title": "Mises à jour",
"description": "Activez pour être averti lorsque de nouvelles mises à jour sont disponibles. jfa-go vérifiera {n} toutes les 30 minutes. Aucune adresse IP ou information personnellement identifiable n'est collectée.",
"updateChannel": "Mettre à jour la chaîne",
"stable": "Stable",
"unstable": "Instable"
}
}

View File

@@ -69,12 +69,12 @@
},
"ombi": {
"title": "Ombi",
"description": "Bij verbinding met Ombi, worden voor nieuwe gebruikers via jfa-go zowel een Jellyfin als een Ombi account aangemaakt. Ga nadat de setup voltooid is naar instellingen om een standaardprofiel voor nieuwe Ombi-gebruikers te kiezen.",
"description": "Bij verbinding met Ombi, wordt voor nieuwe gebruikers via jfa-go zowel een Jellyfin als een Ombi account aangemaakt. Ga nadat de setup voltooid is naar instellingen om een standaardprofiel voor nieuwe Ombi-gebruikers te kiezen.",
"apiKeyNotice": "Te vinden op het eerste tabblad van de Ombi-instellingen."
},
"email": {
"title": "E-mail",
"description": "jfa-go kan wachtwoord reset PINs en meldingen via e-mail versturen. Je kunt verbinding maken met een SMTP server, of de {n} API gebruiken.",
"description": "jfa-go kan wachtwoord reset pincodes en meldingen via e-mail versturen. Je kunt verbinding maken met een SMTP server, of de {n} API gebruiken.",
"method": "Verstuurmethode",
"useEmailAsUsername": "Gebruik e-mailadres als gebruikersnaam",
"useEmailAsUsernameNotice": "Indien ingeschakeld wordt voor nieuwe Jellyfin/Emby-gebruikers hun e-mailadres in plaats van gebruikersnaam gebruikt.",
@@ -99,9 +99,12 @@
},
"passwordResets": {
"title": "Wachtwoordresets",
"description": "Wanneer een gebruiker een wachtwoordreset aanvraagt, maakt Jellyfin een bestand aan dat 'passwordreset-*.json' heet en een PIN bevat. jfa-go leest dit bestand uit en stuurt de PIN naar de gebruiker.",
"description": "Wanneer een gebruiker een wachtwoordreset aanvraagt, maakt Jellyfin een bestand aan dat 'passwordreset-*.json' heet en een pincode bevat. jfa-go leest dit bestand uit en stuurt de pincode naar de gebruiker.",
"pathToJellyfin": "Pad naar Jellyfin configuratiemap",
"pathToJellyfinNotice": "Als je niet weet waar dit is, probeer de je wachtwoord te resetten in Jellyfin. Er verschijnt dan een popup met '<path to jellyfin>/passwordreset-*.json'."
"pathToJellyfinNotice": "Als je niet weet waar dit is, probeer de je wachtwoord te resetten in Jellyfin. Er verschijnt dan een popup met '<path to jellyfin>/passwordreset-*.json'.",
"resetLinks": "Stuur een link in plaats van een pincode",
"resetLinksNotice": "Als Ombi-integratie is ingeschakeld, gebruik dan dit om Jellyfin wachtwoordresets te synchroniseren met Ombi.",
"resetLinksLanguage": "Standaard reset-link taal"
},
"passwordValidation": {
"title": "Wachtwoordvalidatie",

View File

@@ -101,7 +101,10 @@
"title": "Redefinir Senha",
"description": "Quando um usuário tenta redefinir sua senha, o Jellyfin cria um arquivo chamado 'passwordreset-*.json' que contém um PIN. jfa-go lê o arquivo e envia o PIN ao usuário.",
"pathToJellyfin": "Local do diretório de configuração do Jellyfin",
"pathToJellyfinNotice": "Se você não sabe o local onde fica, tente redefinir sua senha no Jellyfin. Um pop-up com '<path to jellyfin>/passwordreset-*.json' aparecerá."
"pathToJellyfinNotice": "Se você não sabe o local onde fica, tente redefinir sua senha no Jellyfin. Um pop-up com '<path to jellyfin>/passwordreset-*.json' aparecerá.",
"resetLinks": "Envie um link em vez de um PIN",
"resetLinksNotice": "Se a integração do Ombi estiver habilitada, use para sincronizar as redefinições de senha do Jellyfin com o Ombi.",
"resetLinksLanguage": "Idioma do link de redefinição padrão"
},
"passwordValidation": {
"title": "Validar Senha",

View File

@@ -69,7 +69,7 @@
},
"ombi": {
"title": "Ombi",
"description": "Genom att ansluta till Ombi skapas både ett Jellyfin- och Ombi-konto när en användare går med genom jfa-go. Efter installationen, om du är klar, gå till Inställningar för att ställa in en standardprofil för nya ombi-användare.",
"description": "Genom att ansluta till Ombi skapas både ett Jellyfin- och Ombi-konto när en användare går med genom jfa-go. Efter installationen är klar, gå till Inställningar för att ställa in en standardprofil för nya ombi-användare.",
"apiKeyNotice": "Hitta detta i den första fliken i Ombi-inställningarna."
},
"email": {
@@ -101,7 +101,10 @@
"title": "Lösenordsåterställningar",
"description": "När en användare försöker återställa sitt lösenord skapar Jellyfin en fil med namnet 'passwordreset-*.json' som innehåller en PIN-kod. jfa-go läser filen och skickar PIN-koden till användaren.",
"pathToJellyfin": "Sökväg till Jellyfin-konfigurationskatalogen",
"pathToJellyfinNotice": "Om du inte vet var det är, försök återställa lösenordet i Jellyfin. En popup med '<sökväg till jellyfin>/passwordreset-*.json' kommer då visas."
"pathToJellyfinNotice": "Om du inte vet var det är, försök återställa lösenordet i Jellyfin. En popup med '<sökväg till jellyfin>/passwordreset-*.json' kommer då visas.",
"resetLinks": "Skicka en länk istället för en PIN",
"resetLinksNotice": "Om Ombi-integrationen är aktiverad, använd den för att synkronisera lösenordsåterställningar med Ombi.",
"resetLinksLanguage": "Förvalt språk för återställningslänk"
},
"passwordValidation": {
"title": "Validering av lösenord",
@@ -123,5 +126,12 @@
"successMessageNotice": "Visas när en användare skapar sitt konto.",
"emailMessage": "E-postmeddelande",
"emailMessageNotice": "Visas längst ner i e-postmeddelanden."
},
"updates": {
"title": "Uppdateringar",
"updateChannel": "Uppdateringskanal",
"description": "Aktivera för att få notifikation när nya uppdateringar finns tillgängliga. jfa-go kollar {n} var 30:e minut. Inga IP-adresser eller personligt identifierande handlingar samlas in.",
"stable": "Stabil",
"unstable": "Ostabil"
}
}

12
lang/telegram/de-DE.json Normal file
View File

@@ -0,0 +1,12 @@
{
"meta": {
"name": "Deutsch (DE)"
},
"strings": {
"startMessage": "Hallo,\ngib deinen Jellyfin-PIN-Code ein, um dein Konto zu verifizieren.",
"matrixStartMessage": "Hallo,\ngib den untenstehenden PIN auf der Anmeldeseite von Jellyfin ein, um dein Konto zu verifizieren.",
"invalidPIN": "Diese PIN war ungültig, versuche es erneut.",
"pinSuccess": "Erfolg! Du kannst nun zur Anmeldeseite zurückkehren.",
"languageMessage": "Hinweis: Zeige verfügbare Sprachen mit {command} an und stelle mit {command} <Sprachcode> die gewünschte ein."
}
}

View File

@@ -4,8 +4,9 @@
},
"strings": {
"startMessage": "Hi!\nEnter your Jellyfin PIN code here to verify your account.",
"matrixStartMessage": "Hi\nEnter the below PIN in the Jellyfin sign-up page to verify your account.",
"invalidPIN": "That PIN was invalid, try again.",
"pinSuccess": "Success! You can now return to the sign-up page.",
"languageMessage": "Note: See available languages with /lang, and set language with /lang <language code>."
"languageMessage": "Note: See available languages with {command}, and set language with {command} <language code>."
}
}

12
lang/telegram/nl-NL.json Normal file
View File

@@ -0,0 +1,12 @@
{
"meta": {
"name": "Nederlands (NL)"
},
"strings": {
"startMessage": "Hallo!\nVoer je Jellyfin pincode in om je account te verifiëren.",
"matrixStartMessage": "Hallo\nVoer onderstaande pincode in op de Jellyfin aanmeldpagina om je account te verifiëren.",
"invalidPIN": "Die pincode was ongeldig, probeer het nogmaals.",
"pinSuccess": "Succes! Je kunt nu teruggaan naar de aanmeldpagina.",
"languageMessage": "Opmerking: Bekijk beschikbare talen met {command}, en stel de taal in met {command} <taalcode>."
}
}

12
lang/telegram/pt-BR.json Normal file
View File

@@ -0,0 +1,12 @@
{
"meta": {
"name": "Português (BR)"
},
"strings": {
"startMessage": "Oi!\nDigite seu código PIN Jellyfin aqui para verificar sua conta.",
"matrixStartMessage": "Oi\nDigite o PIN abaixo na página do Jellyfin para verificar sua conta.",
"invalidPIN": "PIN invalido, tente novamente.",
"pinSuccess": "Concluído. Agora você pode retornar à página de inscrição.",
"languageMessage": "Nota: Veja os idiomas disponíveis com {command} e defina o idioma com {command} <language code>."
}
}

3
linecache/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/hrfee/jfa-go/linecache
go 1.16

66
linecache/linecache.go Normal file
View File

@@ -0,0 +1,66 @@
// Package linecache provides a writer that stores n lines of text at once, overwriting old content as it reaches its capacity. Its contents can be read from with a String() method.
package linecache
import (
"strings"
"sync"
)
// LineCache provides an io.Writer that stores a fixed number of lines of text.
type LineCache struct {
count int
lines [][]byte
current int
lock *sync.Mutex
}
// NewLineCache returns a new line cache of capacity (n) lines.
func NewLineCache(n int) *LineCache {
return &LineCache{
current: 0,
count: n,
lines: make([][]byte, n),
lock: &sync.Mutex{},
}
}
// Write writes a given byte array to the cache.
func (l *LineCache) Write(p []byte) (n int, err error) {
l.lock.Lock()
defer l.lock.Unlock()
lines := strings.Split(string(p), "\n")
for _, line := range lines {
if string(line) == "" {
continue
}
if l.current == l.count {
l.current = 0
}
l.lines[l.current] = []byte(line)
l.current++
}
n = len(p)
return
}
// String returns a string representation of the cache contents.
func (l *LineCache) String() string {
i := 0
if l.lines[l.count-1] != nil && l.current != l.count {
i = l.current
}
out := ""
for {
if l.lines[i] == nil {
return out
}
out += string(l.lines[i]) + "\n"
i++
if i == l.current {
return out
}
if i == l.count {
i = 0
}
}
}

View File

@@ -0,0 +1,17 @@
package linecache
import (
"fmt"
"strings"
"testing"
"time"
)
func Test(t *testing.T) {
wr := NewLineCache(10)
for i := 10; i < 50; i++ {
fmt.Fprintln(wr, i)
fmt.Print(strings.ReplaceAll(wr.String(), "\n", " "), "\n")
time.Sleep(time.Second)
}
}

77
log.go Normal file
View File

@@ -0,0 +1,77 @@
package main
import (
"io"
"log"
"os"
"path/filepath"
"regexp"
"github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/linecache"
)
var logPath string = filepath.Join(temp, "jfa-go.log")
var lineCache = linecache.NewLineCache(100)
func logOutput() (closeFunc func()) {
old := os.Stdout
log.Printf("Logging to \"%s\"", logPath)
writers := []io.Writer{old, colorStripper{lineCache}}
wExit := make(chan bool)
r, w, _ := os.Pipe()
if TRAY {
f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
closeFunc = func() {}
return
}
writers = append(writers, colorStripper{f})
closeFunc = func() {
w.Close()
<-wExit
f.Close()
}
} else {
closeFunc = func() {
w.Close()
<-wExit
}
}
writer := io.MultiWriter(writers...)
os.Stdout, os.Stderr = w, w
log.SetOutput(writer)
gin.DefaultWriter, gin.DefaultErrorWriter = writer, writer
go func() {
io.Copy(writer, r)
wExit <- true
}()
return
}
// Regex that removes ANSI color escape sequences. Used for outputting to log file and log cache.
var stripColors = func() *regexp.Regexp {
r, err := regexp.Compile("\\x1b\\[[0-9;]*m")
if err != nil {
log.Fatalf("Failed to compile color escape regexp: %v", err)
}
return r
}()
type colorStripper struct {
file io.Writer
}
func (c colorStripper) Write(p []byte) (n int, err error) {
_, err = c.file.Write(stripColors.ReplaceAll(p, []byte("")))
n = len(p)
return
}
func sanitizeLog(l string) string {
quoteCensor, err := regexp.Compile("\"([^\"]*)\"")
if err != nil {
log.Fatalf("Failed to compile sanitizing regexp: %v", err)
}
return string(quoteCensor.ReplaceAll([]byte(l), []byte("\"CENSORED\"")))
}

View File

@@ -2,6 +2,7 @@
package logger
import (
"errors"
"io"
"log"
"runtime"
@@ -16,12 +17,14 @@ type Logger interface {
Println(v ...interface{})
Fatal(v ...interface{})
Fatalf(format string, v ...interface{})
SetFatalFunc(f func(err interface{}))
}
type logger struct {
logger *log.Logger
shortfile bool
printer *c.Color
fatalFunc func(err interface{})
}
func Lshortfile() string {
@@ -97,7 +100,16 @@ func (l logger) Fatalf(format string, v ...interface{}) {
out = Lshortfile()
}
out += " " + l.printer.Sprintf(format, v...)
l.logger.Fatal(out)
if l.fatalFunc != nil {
l.logger.Print(out)
l.fatalFunc(errors.New(out))
} else {
l.logger.Fatal(out)
}
}
func (l logger) SetFatalFunc(f func(err interface{})) {
l.fatalFunc = f
}
type EmptyLogger bool
@@ -107,3 +119,4 @@ func (l EmptyLogger) Print(v ...interface{}) {}
func (l EmptyLogger) Println(v ...interface{}) {}
func (l EmptyLogger) Fatal(v ...interface{}) {}
func (l EmptyLogger) Fatalf(format string, v ...interface{}) {}
func (l EmptyLogger) SetFatalFunc(f func(err interface{})) {}

206
main.go
View File

@@ -16,7 +16,6 @@ import (
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
@@ -35,12 +34,15 @@ var (
SOCK string = "jfa-go.sock"
SRV *http.Server
RESTART chan bool
TRAYRESTART chan bool
DATA, CONFIG, HOST *string
PORT *int
DEBUG *bool
PPROF *bool
TEST bool
SWAGGER *bool
QUIT = false
RUNNING = false
warning = color.New(color.FgYellow).SprintfFunc()
info = color.New(color.FgMagenta).SprintfFunc()
hiwhite = color.New(color.FgHiWhite).SprintfFunc()
@@ -94,6 +96,8 @@ type appContext struct {
validator Validator
email *Emailer
telegram *TelegramDaemon
discord *DiscordDaemon
matrix *MatrixDaemon
info, debug, err logger.Logger
host string
port int
@@ -147,6 +151,8 @@ func test(app *appContext) {
}
func start(asDaemon, firstCall bool) {
RUNNING = true
defer func() { RUNNING = false }()
// app encompasses essentially all useful functions.
app := new(appContext)
@@ -167,7 +173,9 @@ func start(asDaemon, firstCall bool) {
}
app.info = logger.NewLogger(os.Stdout, "[INFO] ", log.Ltime, color.FgHiWhite)
app.info.SetFatalFunc(Exit)
app.err = logger.NewLogger(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile, color.FgRed)
app.err.SetFatalFunc(Exit)
app.loadArgs(firstCall)
@@ -192,6 +200,9 @@ func start(asDaemon, firstCall bool) {
app.err.Fatalf("Couldn't copy default config.")
}
app.info.Printf("Copied default configuration to \"%s\"", app.configPath)
tempConfig, _ := ini.Load(app.configPath)
tempConfig.Section("").Key("first_run").SetValue("true")
tempConfig.SaveTo(app.configPath)
}
var debugMode bool
@@ -200,9 +211,8 @@ func start(asDaemon, firstCall bool) {
app.err.Fatalf("Failed to load config file \"%s\": %v", app.configPath, err)
}
// Some message settings have been moved from "email" to "messages", this will switch them.
if app.config.Section("email").Key("use_24h").Value() != "" {
app.migrateEmailConfig()
if app.config.Section("").Key("first_run").MustBool(false) {
firstRun = true
}
app.version = app.config.Section("jellyfin").Key("version").String()
@@ -316,6 +326,10 @@ func start(asDaemon, firstCall bool) {
app.storage.emails_path = app.config.Section("files").Key("emails").String()
if err := app.storage.loadEmails(); err != nil {
app.err.Printf("Failed to load Emails: %v", err)
err := migrateEmailStorage(app)
if err != nil {
app.err.Printf("Failed to migrate Email storage: %v", err)
}
}
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
if err := app.storage.loadPolicy(); err != nil {
@@ -337,26 +351,17 @@ func start(asDaemon, firstCall bool) {
if err := app.storage.loadTelegramUsers(); err != nil {
app.err.Printf("Failed to load Telegram users: %v", err)
}
app.storage.discord_path = app.config.Section("files").Key("discord_users").String()
if err := app.storage.loadDiscordUsers(); err != nil {
app.err.Printf("Failed to load Discord users: %v", err)
}
app.storage.matrix_path = app.config.Section("files").Key("matrix_users").String()
if err := app.storage.loadMatrixUsers(); err != nil {
app.err.Printf("Failed to load Matrix users: %v", err)
}
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
app.storage.loadProfiles()
// Migrate from pre-0.2.0 user templates to profiles
if !(app.storage.policy.BlockedTags == nil && app.storage.configuration.GroupedFolders == nil && len(app.storage.displayprefs) == 0) {
app.info.Println("Migrating user template files to new profile format")
app.storage.migrateToProfile()
for _, path := range [3]string{app.storage.policy_path, app.storage.configuration_path, app.storage.displayprefs_path} {
if _, err := os.Stat(path); !os.IsNotExist(err) {
dir, fname := filepath.Split(path)
newFname := strings.Replace(fname, ".json", ".old.json", 1)
err := os.Rename(path, filepath.Join(dir, newFname))
if err != nil {
app.err.Fatalf("Failed to rename %s: %s", fname, err)
}
}
}
app.info.Println("In case of a problem, your original files have been renamed to <file>.old.json")
app.storage.storeProfiles()
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
app.storage.ombi_path = app.config.Section("files").Key("ombi_template").String()
@@ -375,17 +380,6 @@ func start(asDaemon, firstCall bool) {
configBase, _ := fs.ReadFile(localFS, app.configBasePath)
json.Unmarshal(configBase, &app.configBase)
themes := map[string]string{
"Jellyfin (Dark)": "dark-theme",
"Default (Light)": "light-theme",
}
// For move from Bootstrap to a17t (0.2.5)
if app.config.Section("ui").Key("theme").String() == "Bootstrap (Light)" {
app.config.Section("ui").Key("theme").SetValue("Default (Light)")
}
if val, ok := themes[app.config.Section("ui").Key("theme").String()]; ok {
app.cssClass = val
}
secret, err := generateSecret(16)
if err != nil {
app.err.Fatal(err)
@@ -396,17 +390,17 @@ func start(asDaemon, firstCall bool) {
server := app.config.Section("jellyfin").Key("server").String()
cacheTimeout := int(app.config.Section("jellyfin").Key("cache_timeout").MustUint(30))
stringServerType := app.config.Section("jellyfin").Key("type").String()
timeoutHandler := mediabrowser.NewNamedTimeoutHandler("Jellyfin", server, true)
timeoutHandler := mediabrowser.NewNamedTimeoutHandler("Jellyfin", "\""+server+"\"", true)
if stringServerType == "emby" {
serverType = mediabrowser.EmbyServer
timeoutHandler = mediabrowser.NewNamedTimeoutHandler("Emby", server, true)
timeoutHandler = mediabrowser.NewNamedTimeoutHandler("Emby", "\""+server+"\"", true)
app.info.Println("Using Emby server type")
fmt.Println(warning("WARNING: Emby compatibility is experimental, and support is limited.\nPassword resets are not available."))
} else {
app.info.Println("Using Jellyfin server type")
}
app.jf, _ = mediabrowser.NewServer(
app.jf, err = mediabrowser.NewServer(
serverType,
server,
app.config.Section("jellyfin").Key("client").String(),
@@ -416,85 +410,20 @@ func start(asDaemon, firstCall bool) {
timeoutHandler,
cacheTimeout,
)
if err != nil {
app.err.Fatalf("Failed to authenticate with Jellyfin @ \"%s\": %v", server, err)
}
if debugMode {
app.jf.Verbose = true
}
var status int
_, status, err = app.jf.Authenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String())
if status != 200 || err != nil {
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s (%d): %v", server, status, err)
app.err.Fatalf("Failed to authenticate with Jellyfin @ \"%s\" (%d): %v", server, status, err)
}
app.info.Printf("Authenticated with %s", server)
/* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs.
This checks if the version is equal or higher. */
checkVersion := func(version string) int {
numberStrings := strings.Split(version, ".")
n := 0
for _, s := range numberStrings {
num, err := strconv.Atoi(s)
if err == nil {
n += num
}
}
return n
}
if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") {
// Get users to check if server uses hyphenated userIDs
app.jf.GetUsers(false)
noHyphens := true
for id := range app.storage.emails {
if strings.Contains(id, "-") {
noHyphens = false
break
}
}
if noHyphens == app.jf.Hyphens {
var newEmails map[string]interface{}
var newUsers map[string]time.Time
var status, status2 int
var err, err2 error
if app.jf.Hyphens {
app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match."))
time.Sleep(time.Second * time.Duration(3))
newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails)
newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users)
} else {
app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
time.Sleep(time.Second * time.Duration(3))
newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails)
newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users)
}
if status != 200 || err != nil {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
app.err.Fatalf("Couldn't upgrade emails.json")
}
if status2 != 200 || err2 != nil {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
app.err.Fatalf("Couldn't upgrade users.json")
}
emailBakFile := app.storage.emails_path + ".bak"
usersBakFile := app.storage.users_path + ".bak"
err = storeJSON(emailBakFile, app.storage.emails)
err2 = storeJSON(usersBakFile, app.storage.users)
if err != nil {
app.err.Fatalf("couldn't store emails.json backup: %v", err)
}
if err2 != nil {
app.err.Fatalf("couldn't store users.json backup: %v", err)
}
app.storage.emails = newEmails
app.storage.users = newUsers
err = app.storage.storeEmails()
err2 = app.storage.storeUsers()
if err != nil {
app.err.Fatalf("couldn't store emails.json: %v", err)
}
if err2 != nil {
app.err.Fatalf("couldn't store users.json: %v", err)
}
}
}
runMigrations(app)
// Auth (manual user/pass or jellyfin)
app.jellyfinLogin = true
@@ -540,7 +469,7 @@ func start(asDaemon, firstCall bool) {
invDaemon := newInviteDaemon(time.Duration(60*time.Second), app)
go invDaemon.run()
defer invDaemon.shutdown()
defer invDaemon.Shutdown()
userDaemon := newUserDaemon(time.Duration(60*time.Second), app)
go userDaemon.run()
@@ -558,11 +487,32 @@ func start(asDaemon, firstCall bool) {
app.telegram, err = newTelegramDaemon(app)
if err != nil {
app.err.Printf("Failed to authenticate with Telegram: %v", err)
telegramEnabled = false
} else {
go app.telegram.run()
defer app.telegram.Shutdown()
}
}
if discordEnabled {
app.discord, err = newDiscordDaemon(app)
if err != nil {
app.err.Printf("Failed to authenticate with Discord: %v", err)
discordEnabled = false
} else {
go app.discord.run()
defer app.discord.Shutdown()
}
}
if matrixEnabled {
app.matrix, err = newMatrixDaemon(app)
if err != nil {
app.err.Printf("Failed to initialize Matrix daemon: %v", err)
matrixEnabled = false
} else {
go app.matrix.run()
defer app.matrix.Shutdown()
}
}
} else {
debugMode = false
address = "0.0.0.0:8056"
@@ -599,6 +549,11 @@ func start(asDaemon, firstCall bool) {
}
}
}()
if firstRun {
app.info.Printf("Loaded, visit %s to start.", address)
} else {
app.info.Printf("Loaded @ %s", address)
}
app.quit = make(chan os.Signal)
signal.Notify(app.quit, os.Interrupt)
go func() {
@@ -618,13 +573,19 @@ func start(asDaemon, firstCall bool) {
func (app *appContext) shutdown() {
app.info.Println("Shutting down...")
cntx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
if err := SRV.Shutdown(cntx); err != nil {
app.err.Fatalf("Server shutdown error: %s", err)
QUIT = true
RESTART <- true
for {
if RUNNING {
continue
}
cntx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
if err := SRV.Shutdown(cntx); err != nil {
app.err.Fatalf("Server shutdown error: %s", err)
}
os.Exit(1)
}
os.Exit(1)
}
func flagPassed(name string) (found bool) {
@@ -637,10 +598,10 @@ func flagPassed(name string) (found bool) {
}
// @title jfa-go internal API
// @version 0.2.0
// @version 0.3.6
// @description API for the jfa-go frontend
// @contact.name Harvey Tindall
// @contact.email hrfee@protonmail.ch
// @contact.email hrfee@hrfee.dev
// @license.name MIT
// @license.url https://raw.githubusercontent.com/hrfee/jfa-go/main/LICENSE
// @BasePath /
@@ -674,10 +635,20 @@ func flagPassed(name string) (found bool) {
// @tag.description Things that dont fit elsewhere.
func printVersion() {
fmt.Println(info("jfa-go version: %s (%s)\n", hiwhite(version), white(commit)))
tray := ""
if TRAY {
tray = " TrayIcon"
}
fmt.Println(info("jfa-go version: %s (%s)%s\n", hiwhite(version), white(commit), tray))
}
func main() {
defer func() {
if r := recover(); r != nil {
Exit(r)
}
}()
defer logOutput()()
printVersion()
SOCK = filepath.Join(temp, SOCK)
fmt.Println("Socket:", SOCK)
@@ -745,10 +716,15 @@ You can then run:
fmt.Print(info("systemctl --user stop jfa-go\n\n"))
color.New(color.FgYellow).PrintFunc()("To restart: ")
fmt.Print(info("systemctl --user stop jfa-go\n"))
} else if TRAY {
RunTray()
} else {
RESTART = make(chan bool, 1)
start(false, true)
for {
if QUIT {
continue
}
printVersion()
start(false, false)
}

238
matrix.go Normal file
View File

@@ -0,0 +1,238 @@
package main
import (
"encoding/json"
"fmt"
"strings"
"github.com/gomarkdown/markdown"
"github.com/matrix-org/gomatrix"
)
type MatrixDaemon struct {
Stopped bool
ShutdownChannel chan string
bot *gomatrix.Client
userID string
tokens map[string]UnverifiedUser // Map of tokens to users
languages map[string]string // Map of roomIDs to language codes
app *appContext
}
type UnverifiedUser struct {
Verified bool
User *MatrixUser
}
type MatrixUser struct {
RoomID string
UserID string
Lang string
Contact bool
}
type MatrixIdentifier struct {
User string `json:"user"`
IdentType string `json:"type"`
}
func (m MatrixIdentifier) Type() string { return m.IdentType }
var matrixFilter = gomatrix.Filter{
Room: gomatrix.RoomFilter{
Timeline: gomatrix.FilterPart{
Types: []string{
"m.room.message",
"m.room.member",
},
},
},
EventFields: []string{
"type",
"event_id",
"room_id",
"state_key",
"sender",
"content.body",
"content.membership",
},
}
func newMatrixDaemon(app *appContext) (d *MatrixDaemon, err error) {
matrix := app.config.Section("matrix")
homeserver := matrix.Key("homeserver").String()
token := matrix.Key("token").String()
d = &MatrixDaemon{
ShutdownChannel: make(chan string),
userID: matrix.Key("user_id").String(),
tokens: map[string]UnverifiedUser{},
languages: map[string]string{},
app: app,
}
d.bot, err = gomatrix.NewClient(homeserver, d.userID, token)
if err != nil {
return
}
filter, err := json.Marshal(matrixFilter)
if err != nil {
return
}
resp, err := d.bot.CreateFilter(filter)
d.bot.Store.SaveFilterID(d.userID, resp.FilterID)
for _, user := range app.storage.matrix {
if user.Lang != "" {
d.languages[user.RoomID] = user.Lang
}
}
return
}
func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string) (string, error) {
req := &gomatrix.ReqLogin{
Type: "m.login.password",
Identifier: MatrixIdentifier{
User: username,
IdentType: "m.id.user",
},
Password: password,
DeviceID: "jfa-go-" + commit,
}
bot, err := gomatrix.NewClient(homeserver, username, "")
if err != nil {
return "", err
}
resp, err := bot.Login(req)
if err != nil {
return "", err
}
return resp.AccessToken, nil
}
func (d *MatrixDaemon) run() {
d.app.info.Println("Starting Matrix bot daemon")
syncer := d.bot.Syncer.(*gomatrix.DefaultSyncer)
syncer.OnEventType("m.room.message", d.handleMessage)
// syncer.OnEventType("m.room.member", d.handleMembership)
if err := d.bot.Sync(); err != nil {
d.app.err.Printf("Matrix sync failed: %v", err)
}
}
func (d *MatrixDaemon) Shutdown() {
d.bot.StopSync()
d.Stopped = true
close(d.ShutdownChannel)
}
func (d *MatrixDaemon) handleMessage(event *gomatrix.Event) {
if event.Sender == d.userID {
return
}
lang := "en-us"
if l, ok := d.languages[event.RoomID]; ok {
if _, ok := d.app.storage.lang.Telegram[l]; ok {
lang = l
}
}
sects := strings.Split(event.Content["body"].(string), " ")
switch sects[0] {
case "!lang":
if len(sects) == 2 {
d.commandLang(event, sects[1], lang)
} else {
d.commandLang(event, "", lang)
}
}
}
func (d *MatrixDaemon) commandLang(event *gomatrix.Event, code, lang string) {
if code == "" {
list := "!lang <lang>\n"
for c := range d.app.storage.lang.Telegram {
list += fmt.Sprintf("%s: %s\n", c, d.app.storage.lang.Telegram[c].Meta.Name)
}
_, err := d.bot.SendText(
event.RoomID,
list,
)
if err != nil {
d.app.err.Printf("Matrix: Failed to send message to \"%s\": %v", event.Sender, err)
}
return
}
if _, ok := d.app.storage.lang.Telegram[code]; !ok {
return
}
d.languages[event.RoomID] = code
if u, ok := d.app.storage.matrix[event.RoomID]; ok {
u.Lang = code
d.app.storage.matrix[event.RoomID] = u
if err := d.app.storage.storeMatrixUsers(); err != nil {
d.app.err.Printf("Matrix: Failed to store Matrix users: %v", err)
}
}
}
func (d *MatrixDaemon) CreateRoom(userID string) (string, error) {
room, err := d.bot.CreateRoom(&gomatrix.ReqCreateRoom{
Visibility: "private",
Invite: []string{userID},
Topic: d.app.config.Section("matrix").Key("topic").String(),
})
if err != nil {
return "", err
}
return room.RoomID, nil
}
func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
roomID, err := d.CreateRoom(userID)
if err != nil {
d.app.err.Printf("Failed to create room for user \"%s\": %v", userID, err)
return
}
lang := "en-us"
pin := genAuthToken()
d.tokens[pin] = UnverifiedUser{
false,
&MatrixUser{
RoomID: roomID,
UserID: userID,
Lang: lang,
},
}
_, err = d.bot.SendText(
roomID,
d.app.storage.lang.Telegram[lang].Strings.get("matrixStartMessage")+"\n\n"+pin+"\n\n"+
d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"}),
)
if err != nil {
d.app.err.Printf("Matrix: Failed to send welcome message to \"%s\": %v", userID, err)
return
}
ok = true
return
}
func (d *MatrixDaemon) Send(message *Message, roomID ...string) (err error) {
md := ""
if message.Markdown != "" {
// Convert images to links
md = string(markdown.ToHTML([]byte(strings.ReplaceAll(message.Markdown, "![", "[")), nil, renderer))
}
for _, id := range roomID {
if md != "" {
_, err = d.bot.SendFormattedText(id, message.Text, md)
} else {
_, err = d.bot.SendText(id, message.Text)
}
if err != nil {
return
}
}
return
}
// User enters ID on sign-up, a PIN is sent to them. They enter it on sign-up.
// Message the user first, to avoid E2EE by default

180
migrations.go Normal file
View File

@@ -0,0 +1,180 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/ini.v1"
)
func runMigrations(app *appContext) {
migrateProfiles(app)
migrateBootstrap(app)
migrateEmailStorage(app)
// migrateHyphens(app)
}
// Migrate pre-0.2.0 user templates to profiles
func migrateProfiles(app *appContext) {
if !(app.storage.policy.BlockedTags == nil && app.storage.configuration.GroupedFolders == nil && len(app.storage.displayprefs) == 0) {
app.info.Println("Migrating user template files to new profile format")
app.storage.migrateToProfile()
for _, path := range [3]string{app.storage.policy_path, app.storage.configuration_path, app.storage.displayprefs_path} {
if _, err := os.Stat(path); !os.IsNotExist(err) {
dir, fname := filepath.Split(path)
newFname := strings.Replace(fname, ".json", ".old.json", 1)
err := os.Rename(path, filepath.Join(dir, newFname))
if err != nil {
app.err.Fatalf("Failed to rename %s: %s", fname, err)
}
}
}
app.info.Println("In case of a problem, your original files have been renamed to <file>.old.json")
app.storage.storeProfiles()
}
}
// Migrate pre-0.2.5 bootstrap theme choice to a17t version.
func migrateBootstrap(app *appContext) {
themes := map[string]string{
"Jellyfin (Dark)": "dark-theme",
"Default (Light)": "light-theme",
}
if app.config.Section("ui").Key("theme").String() == "Bootstrap (Light)" {
app.config.Section("ui").Key("theme").SetValue("Default (Light)")
}
if val, ok := themes[app.config.Section("ui").Key("theme").String()]; ok {
app.cssClass = val
}
}
func migrateEmailConfig(app *appContext) {
tempConfig, _ := ini.Load(app.configPath)
fmt.Println(warning("Part of your email configuration will be migrated to the new \"messages\" section.\nA backup will be made."))
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
if err != nil {
app.err.Fatalf("Failed to backup config: %v", err)
return
}
for _, setting := range []string{"use_24h", "date_format", "message"} {
if val := app.config.Section("email").Key(setting).Value(); val != "" {
tempConfig.Section("email").Key(setting).SetValue("")
tempConfig.Section("messages").Key(setting).SetValue(val)
}
}
if app.config.Section("messages").Key("enabled").MustBool(false) || app.config.Section("telegram").Key("enabled").MustBool(false) {
tempConfig.Section("messages").Key("enabled").SetValue("true")
}
err = tempConfig.SaveTo(app.configPath)
if err != nil {
app.err.Fatalf("Failed to save config: %v", err)
return
}
app.loadConfig()
}
// Migrate pre-0.3.6 email settings to the new messages section.
// Called just after loading email storage in main.go.
func migrateEmailStorage(app *appContext) error {
// use_24h was moved to messages, so this checks if migration has already occurred or not.
if app.config.Section("email").Key("use_24h").Value() == "" {
return nil
}
var emails map[string]interface{}
err := loadJSON(app.storage.emails_path, &emails)
if err != nil {
return err
}
newEmails := map[string]EmailAddress{}
for jfID, addr := range emails {
newEmails[jfID] = EmailAddress{
Addr: addr.(string),
Contact: true,
}
}
err = storeJSON(app.storage.emails_path+".bak", emails)
if err != nil {
return err
}
err = storeJSON(app.storage.emails_path, newEmails)
if err != nil {
return err
}
app.info.Println("Migrated to new email format. A backup has also been made.")
return nil
}
// Migrate between hyphenated & non-hyphenated user IDs. Doesn't seem to happen anymore, so disabled.
// func migrateHyphens(app *appContext) {
// checkVersion := func(version string) int {
// numberStrings := strings.Split(version, ".")
// n := 0
// for _, s := range numberStrings {
// num, err := strconv.Atoi(s)
// if err == nil {
// n += num
// }
// }
// return n
// }
// if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") {
// // Get users to check if server uses hyphenated userIDs
// app.jf.GetUsers(false)
//
// noHyphens := true
// for id := range app.storage.emails {
// if strings.Contains(id, "-") {
// noHyphens = false
// break
// }
// }
// if noHyphens == app.jf.Hyphens {
// var newEmails map[string]interface{}
// var newUsers map[string]time.Time
// var status, status2 int
// var err, err2 error
// if app.jf.Hyphens {
// app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match."))
// time.Sleep(time.Second * time.Duration(3))
// newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails)
// newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users)
// } else {
// app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
// time.Sleep(time.Second * time.Duration(3))
// newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails)
// newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users)
// }
// if status != 200 || err != nil {
// app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
// app.err.Fatalf("Couldn't upgrade emails.json")
// }
// if status2 != 200 || err2 != nil {
// app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
// app.err.Fatalf("Couldn't upgrade users.json")
// }
// emailBakFile := app.storage.emails_path + ".bak"
// usersBakFile := app.storage.users_path + ".bak"
// err = storeJSON(emailBakFile, app.storage.emails)
// err2 = storeJSON(usersBakFile, app.storage.users)
// if err != nil {
// app.err.Fatalf("couldn't store emails.json backup: %v", err)
// }
// if err2 != nil {
// app.err.Fatalf("couldn't store users.json backup: %v", err)
// }
// app.storage.emails = newEmails
// app.storage.users = newUsers
// err = app.storage.storeEmails()
// err2 = app.storage.storeUsers()
// if err != nil {
// app.err.Fatalf("couldn't store emails.json: %v", err)
// }
// if err2 != nil {
// app.err.Fatalf("couldn't store users.json: %v", err)
// }
// }
// }
// }

View File

@@ -17,6 +17,10 @@ type newUserDTO struct {
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
TelegramPIN string `json:"telegram_pin" example:"A1-B2-3C"` // Telegram verification PIN (if used)
TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs
DiscordPIN string `json:"discord_pin" example:"A1-B2-3C"` // Discord verification PIN (if used)
DiscordContact bool `json:"discord_contact"` // Whether or not to use discord for notifications/pwrs
MatrixPIN string `json:"matrix_pin" example:"A1-B2-3C"` // Matrix verification PIN (if used)
MatrixContact bool `json:"matrix_contact"` // Whether or not to use matrix for notifications/pwrs
}
type newUserResponse struct {
@@ -48,7 +52,7 @@ type generateInviteDTO struct {
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
Email string `json:"email" example:"jeff@jellyf.in"` // Send invite to this address
SendTo string `json:"send-to" example:"jeff@jellyf.in"` // Send invite to this address or discord name
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
@@ -98,7 +102,7 @@ type inviteDTO struct {
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)
Email string `json:"email,omitempty"` // Email the invite was sent to (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
@@ -125,12 +129,18 @@ type respUser struct {
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
Name string `json:"name" example:"jeff"` // Username of user
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
Telegram string `json:"telegram"` // Telegram username (if known)
NotifyThroughEmail bool `json:"notify_email"`
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
Telegram string `json:"telegram"` // Telegram username (if known)
NotifyThroughTelegram bool `json:"notify_telegram"`
Discord string `json:"discord"` // Discord username (if known)
DiscordID string `json:"discord_id"` // Discord user ID for creating links.
NotifyThroughDiscord bool `json:"notify_discord"`
Matrix string `json:"matrix"` // Matrix ID (if known)
NotifyThroughMatrix bool `json:"notify_matrix"`
}
type getUsersDTO struct {
@@ -249,7 +259,54 @@ type telegramSetDTO struct {
ID string `json:"id"` // Jellyfin ID of user.
}
type telegramNotifyDTO struct {
ID string `json:"id"`
Enabled bool `json:"enabled"`
type SetContactMethodsDTO struct {
ID string `json:"id"`
Email bool `json:"email"`
Discord bool `json:"discord"`
Telegram bool `json:"telegram"`
Matrix bool `json:"matrix"`
}
type DiscordUserDTO struct {
Name string `json:"name"`
AvatarURL string `json:"avatar_url"`
ID string `json:"id"`
}
type DiscordUsersDTO struct {
Users []DiscordUserDTO `json:"users"`
}
type DiscordConnectUserDTO struct {
JellyfinID string `json:"jf_id"`
DiscordID string `json:"discord_id"`
}
type DiscordInviteDTO struct {
InviteURL string `json:"invite"`
IconURL string `json:"icon"`
}
type MatrixSendPINDTO struct {
UserID string `json:"user_id"`
}
type MatrixCheckPINDTO struct {
PIN string `json:"pin"`
}
type MatrixConnectUserDTO struct {
JellyfinID string `json:"jf_id"`
UserID string `json:"user_id"`
}
type MatrixLoginDTO struct {
Homeserver string `json:"homeserver"`
Username string `json:"username"`
Password string `json:"password"`
}
type ResetPasswordDTO struct {
PIN string `json:"pin"`
Password string `json:"password"`
}

7
notray.go Normal file
View File

@@ -0,0 +1,7 @@
// +build !tray
package main
var TRAY = false
func RunTray() {}

6606
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,11 +20,14 @@
"@ts-stack/markdown": "^1.3.0",
"@types/node": "^15.0.1",
"a17t": "^0.4.0",
"browserslist": "^4.16.6",
"esbuild": "^0.8.57",
"lodash": "^4.17.19",
"inline-source-cli": "^2.0.0",
"lodash": "^4.17.21",
"mjml": "^4.8.0",
"remixicon": "^2.5.0",
"remove-markdown": "^0.3.0",
"typescript": "^4.0.3"
"typescript": "^4.0.3",
"uncss": "^0.17.3"
}
}

View File

@@ -54,10 +54,12 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
var pwr PasswordReset
data, err := os.ReadFile(event.Name)
if err != nil {
app.debug.Printf("PWR: Failed to read file: %v", err)
return
}
err = json.Unmarshal(data, &pwr)
if len(pwr.Pin) == 0 || err != nil {
app.debug.Printf("PWR: Failed to read PIN: %v", err)
return
}
app.info.Printf("New password reset for user \"%s\"", pwr.Username)

View File

@@ -108,6 +108,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
if app.config.Section("password_resets").Key("link_reset").MustBool(false) {
router.GET(p+"/reset", app.ResetPassword)
if app.config.Section("password_resets").Key("set_password").MustBool(false) {
router.POST(p+"/reset", app.ResetSetPassword)
}
}
router.GET(p+"/accounts", app.AdminPage)
@@ -121,6 +124,17 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
if telegramEnabled {
router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
}
if discordEnabled {
router.GET(p+"/invite/:invCode/discord/verified/:pin", app.DiscordVerifiedInvite)
if app.config.Section("discord").Key("provide_invite").MustBool(false) {
router.GET(p+"/invite/:invCode/discord/invite", app.DiscordServerInvite)
}
}
if matrixEnabled {
router.GET(p+"/invite/:invCode/matrix/verified/:userID/:pin", app.MatrixCheckPIN)
router.POST(p+"/invite/:invCode/matrix/user", app.MatrixSendPIN)
router.POST(p+"/users/matrix", app.MatrixConnect)
}
}
if *SWAGGER {
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
@@ -158,16 +172,22 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/config", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart)
if telegramEnabled {
if telegramEnabled || discordEnabled || matrixEnabled {
api.GET(p+"/telegram/pin", app.TelegramGetPin)
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
api.POST(p+"/users/telegram", app.TelegramAddUser)
api.POST(p+"/users/telegram/notify", app.TelegramSetNotify)
api.POST(p+"/users/contact", app.SetContactMethods)
}
if discordEnabled {
api.GET(p+"/users/discord/:username", app.DiscordGetUsers)
api.POST(p+"/users/discord", app.DiscordConnect)
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
api.GET(p+"/ombi/users", app.OmbiUsers)
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)
}
api.POST(p+"/matrix/login", app.MatrixLogin)
}
}

View File

@@ -4,6 +4,8 @@ import json
import argparse
from pathlib import Path
def fix_description(desc):
return "; " + desc.replace("\n", "\n; ")
def generate_ini(base_file, ini_file):
"""
@@ -17,10 +19,10 @@ def generate_ini(base_file, ini_file):
for section in config_base["sections"]:
ini.add_section(section)
if "meta" in config_base["sections"][section]:
ini.set(section, "; " + config_base["sections"][section]["meta"]["description"])
ini.set(section, fix_description(config_base["sections"][section]["meta"]["description"]))
for entry in config_base["sections"][section]["settings"]:
if "description" in config_base["sections"][section]["settings"][entry]:
ini.set(section, "; " + config_base["sections"][section]["settings"][entry]["description"])
ini.set(section, fix_description(config_base["sections"][section]["settings"][entry]["description"]))
value = config_base["sections"][section]["settings"][entry]["value"]
if isinstance(value, bool):
value = str(value).lower()

View File

@@ -2,4 +2,4 @@
# sets version environment variable for goreleaser to use
# scripts/version.sh goreleaser ...
JFA_GO_VERSION=$(git describe --exact-match HEAD 2> /dev/null || echo 'vgit')
JFA_GO_VERSION="$(echo $JFA_GO_VERSION | sed 's/v//g')" $@
JFA_GO_NFPM_EPOCH=$(git rev-list --all --count) JFA_GO_VERSION="$(echo $JFA_GO_VERSION | sed 's/v//g')" $@

View File

@@ -15,19 +15,22 @@ import (
)
type Storage struct {
timePattern string
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path string
users map[string]time.Time
invites Invites
profiles map[string]Profile
defaultProfile string
emails, displayprefs, ombi_template map[string]interface{}
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
customEmails customEmails
policy mediabrowser.Policy
configuration mediabrowser.Configuration
lang Lang
invitesLock, usersLock sync.Mutex
timePattern string
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path string
users map[string]time.Time
invites Invites
profiles map[string]Profile
defaultProfile string
displayprefs, ombi_template map[string]interface{}
emails map[string]EmailAddress
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
matrix map[string]MatrixUser // Map of Jellyfin user IDs to Matrix users.
customEmails customEmails
policy mediabrowser.Policy
configuration mediabrowser.Configuration
lang Lang
invitesLock, usersLock sync.Mutex
}
type TelegramUser struct {
@@ -37,6 +40,20 @@ type TelegramUser struct {
Contact bool // Whether to contact through telegram or not
}
type DiscordUser struct {
ChannelID string
ID string
Username string
Discriminator string
Lang string
Contact bool
}
type EmailAddress struct {
Addr string
Contact bool
}
type customEmails struct {
UserCreated customEmail `json:"userCreated"`
InviteExpiry customEmail `json:"inviteExpiry"`
@@ -79,7 +96,7 @@ type Invite struct {
UserDays int `json:"user-days,omitempty"`
UserHours int `json:"user-hours,omitempty"`
UserMinutes int `json:"user-minutes,omitempty"`
Email string `json:"email"`
SendTo string `json:"email"`
// Used to be stored as formatted time, now as Unix.
UsedBy [][]string `json:"used-by"`
Notify map[string]map[string]bool `json:"notify"`
@@ -89,23 +106,24 @@ type Invite struct {
}
type Lang struct {
AdminPath string
chosenAdminLang string
Admin adminLangs
AdminJSON map[string]string
FormPath string
chosenFormLang string
Form formLangs
PasswordResetPath string
chosenPWRLang string
PasswordReset pwrLangs
EmailPath string
chosenEmailLang string
Email emailLangs
CommonPath string
Common commonLangs
SetupPath string
Setup setupLangs
AdminPath string
chosenAdminLang string
Admin adminLangs
AdminJSON map[string]string
FormPath string
chosenFormLang string
Form formLangs
PasswordResetPath string
chosenPWRLang string
PasswordReset pwrLangs
EmailPath string
chosenEmailLang string
Email emailLangs
CommonPath string
Common commonLangs
SetupPath string
Setup setupLangs
// Telegram translations are also used for Discord bots (and likely future ones).
chosenTelegramLang string
TelegramPath string
Telegram telegramLangs
@@ -765,6 +783,22 @@ func (st *Storage) storeTelegramUsers() error {
return storeJSON(st.telegram_path, st.telegram)
}
func (st *Storage) loadDiscordUsers() error {
return loadJSON(st.discord_path, &st.discord)
}
func (st *Storage) storeDiscordUsers() error {
return storeJSON(st.discord_path, st.discord)
}
func (st *Storage) loadMatrixUsers() error {
return loadJSON(st.matrix_path, &st.matrix)
}
func (st *Storage) storeMatrixUsers() error {
return storeJSON(st.matrix_path, st.matrix)
}
func (st *Storage) loadCustomEmails() error {
return loadJSON(st.customEmails_path, &st.customEmails)
}
@@ -883,86 +917,3 @@ func storeJSON(path string, obj interface{}) error {
}
return err
}
// One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage.
func hyphenate(userID string) string {
if userID[8] == '-' {
return userID
}
return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
}
func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
jfUsers, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil {
return nil, status, err
}
newEmails := map[string]interface{}{}
for _, user := range jfUsers {
unHyphenated := user.ID
hyphenated := hyphenate(unHyphenated)
val, ok := old[hyphenated]
if ok {
newEmails[unHyphenated] = val
}
}
return newEmails, status, err
}
func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
jfUsers, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil {
return nil, status, err
}
newEmails := map[string]interface{}{}
for _, user := range jfUsers {
unstripped := user.ID
stripped := strings.ReplaceAll(unstripped, "-", "")
val, ok := old[stripped]
if ok {
newEmails[unstripped] = val
}
}
return newEmails, status, err
}
func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
return app.hyphenateStorage(old)
}
func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
return app.deHyphenateStorage(old)
}
func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
asInterface := map[string]interface{}{}
for k, v := range old {
asInterface[k] = v
}
fixed, status, err := app.hyphenateStorage(asInterface)
if err != nil {
return nil, status, err
}
out := map[string]time.Time{}
for k, v := range fixed {
out[k] = v.(time.Time)
}
return out, status, err
}
func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
asInterface := map[string]interface{}{}
for k, v := range old {
asInterface[k] = v
}
fixed, status, err := app.deHyphenateStorage(asInterface)
if err != nil {
return nil, status, err
}
out := map[string]time.Time{}
for k, v := range fixed {
out[k] = v.(time.Time)
}
return out, status, err
}

View File

@@ -3,22 +3,57 @@ package main
import (
"strings"
dg "github.com/bwmarrin/discordgo"
stripmd "github.com/writeas/go-strip-markdown"
)
type Link struct {
Alt, URL string
}
// StripAltText removes Markdown alt text from links and images and replaces them with just the URL.
// Currently uses the deepest alt text when links/images are nested.
func StripAltText(md string) string {
// If links = true, links are completely removed, and a list of URLs and their alt text is also returned.
func StripAltText(md string, links bool) (string, []*dg.MessageEmbed) {
altTextStart := -1 // Start of alt text (between '[' & ']')
URLStart := -1 // Start of url (between '(' & ')')
URLEnd := -1
previousURLEnd := -2
out := ""
embeds := []*dg.MessageEmbed{}
for i := range md {
if altTextStart != -1 && URLStart != -1 && md[i] == ')' {
URLEnd = i - 1
out += md[previousURLEnd+2:altTextStart-1] + md[URLStart:URLEnd+1]
out += md[previousURLEnd+2 : altTextStart-1]
if links {
embed := &dg.MessageEmbed{
Type: dg.EmbedTypeLink,
Title: md[altTextStart : URLStart-2],
}
if md[altTextStart-1] == '!' {
embed.Title = md[altTextStart+1 : URLStart-2]
embed.Type = dg.EmbedTypeImage
embed.Image = &dg.MessageEmbedImage{
URL: md[URLStart : URLEnd+1],
}
} else {
embed.URL = md[URLStart : URLEnd+1]
}
embeds = append(embeds, embed)
} else {
out += md[URLStart : URLEnd+1]
}
previousURLEnd = URLEnd
// Removing links often leaves a load of extra newlines which look weird, this removes them.
if links {
next := 2
for md[URLEnd+next] == '\n' {
next++
}
if next >= 3 {
previousURLEnd += next - 2
}
}
altTextStart, URLStart, URLEnd = -1, -1, -1
continue
}
@@ -36,11 +71,12 @@ func StripAltText(md string) string {
out += md[previousURLEnd+2:]
}
if out == "" {
return md
return md, embeds
}
return out
return out, embeds
}
func stripMarkdown(md string) string {
return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(StripAltText(md)), "</p>"), "<p>")
stripped, _ := StripAltText(md, false)
return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(stripped), "</p>"), "<p>")
}

View File

@@ -9,7 +9,7 @@ import (
tg "github.com/go-telegram-bot-api/telegram-bot-api"
)
type VerifiedToken struct {
type TelegramVerifiedToken struct {
Token string
ChatID int64
Username string
@@ -21,7 +21,7 @@ type TelegramDaemon struct {
bot *tg.BotAPI
username string
tokens []string
verifiedTokens []VerifiedToken
verifiedTokens []TelegramVerifiedToken
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
link string
app *appContext
@@ -37,12 +37,11 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
return nil, err
}
td := &TelegramDaemon{
Stopped: false,
ShutdownChannel: make(chan string),
bot: bot,
username: bot.Self.UserName,
tokens: []string{},
verifiedTokens: []VerifiedToken{},
verifiedTokens: []TelegramVerifiedToken{},
languages: map[int64]string{},
link: "https://t.me/" + bot.Self.UserName,
app: app,
@@ -55,23 +54,28 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
return td, nil
}
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
func (t *TelegramDaemon) NewAuthToken() string {
func genAuthToken() string {
rand.Seed(time.Now().UnixNano())
pin := make([]rune, 8)
for i := range pin {
if i == 2 || i == 5 {
if (i+1)%3 == 0 {
pin[i] = '-'
} else {
pin[i] = runes[rand.Intn(len(runes))]
}
}
t.tokens = append(t.tokens, string(pin))
return string(pin)
}
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
func (t *TelegramDaemon) NewAuthToken() string {
pin := genAuthToken()
t.tokens = append(t.tokens, pin)
return pin
}
func (t *TelegramDaemon) run() {
t.app.info.Println("Starting Telegram bot daemon")
u := tg.NewUpdate(0)
@@ -79,6 +83,7 @@ func (t *TelegramDaemon) run() {
updates, err := t.bot.GetUpdatesChan(u)
if err != nil {
t.app.err.Printf("Failed to start Telegram daemon: %v", err)
telegramEnabled = false
return
}
for {
@@ -111,66 +116,13 @@ func (t *TelegramDaemon) run() {
}
switch msg := sects[0]; msg {
case "/start":
content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
content += t.app.storage.lang.Telegram[lang].Strings.get("languageMessage")
err := t.Reply(&upd, content)
if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
t.commandStart(&upd, sects, lang)
continue
case "/lang":
if len(sects) == 1 {
list := "/lang <lang>\n"
for code := range t.app.storage.lang.Telegram {
list += fmt.Sprintf("%s: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name)
}
err := t.Reply(&upd, list)
if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
continue
}
if _, ok := t.app.storage.lang.Telegram[sects[1]]; ok {
t.languages[upd.Message.Chat.ID] = sects[1]
for jfID, user := range t.app.storage.telegram {
if user.ChatID == upd.Message.Chat.ID {
user.Lang = sects[1]
t.app.storage.telegram[jfID] = user
err := t.app.storage.storeTelegramUsers()
if err != nil {
t.app.err.Printf("Failed to store Telegram users: %v", err)
}
break
}
}
}
t.commandLang(&upd, sects, lang)
continue
default:
tokenIndex := -1
for i, token := range t.tokens {
if upd.Message.Text == token {
tokenIndex = i
break
}
}
if tokenIndex == -1 {
err := t.QuoteReply(&upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
continue
}
err := t.QuoteReply(&upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
t.verifiedTokens = append(t.verifiedTokens, VerifiedToken{
Token: upd.Message.Text,
ChatID: upd.Message.Chat.ID,
Username: upd.Message.Chat.UserName,
})
t.tokens[len(t.tokens)-1], t.tokens[tokenIndex] = t.tokens[tokenIndex], t.tokens[len(t.tokens)-1]
t.tokens = t.tokens[:len(t.tokens)-1]
t.commandPIN(&upd, sects, lang)
}
case <-t.ShutdownChannel:
@@ -194,6 +146,9 @@ func (t *TelegramDaemon) QuoteReply(upd *tg.Update, content string) error {
return err
}
var escapedChars = []string{"_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(", "\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>", "#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|", "\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!"}
var escaper = strings.NewReplacer(escapedChars...)
// Send will send a telegram message to a list of chat IDs. message.text is used if no markdown is given.
func (t *TelegramDaemon) Send(message *Message, ID ...int64) error {
for _, id := range ID {
@@ -201,7 +156,8 @@ func (t *TelegramDaemon) Send(message *Message, ID ...int64) error {
if message.Markdown == "" {
msg = tg.NewMessage(id, message.Text)
} else {
msg = tg.NewMessage(id, strings.ReplaceAll(message.Markdown, ".", "\\."))
text := escaper.Replace(message.Markdown)
msg = tg.NewMessage(id, text)
msg.ParseMode = "MarkdownV2"
}
_, err := t.bot.Send(msg)
@@ -218,3 +174,67 @@ func (t *TelegramDaemon) Shutdown() {
<-t.ShutdownChannel
close(t.ShutdownChannel)
}
func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang string) {
content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
content += t.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "/lang"})
err := t.Reply(upd, content)
if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
}
func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string) {
if len(sects) == 1 {
list := "/lang `<lang>`\n"
for code := range t.app.storage.lang.Telegram {
list += fmt.Sprintf("`%s`: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name)
}
err := t.Reply(upd, list)
if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
return
}
if _, ok := t.app.storage.lang.Telegram[sects[1]]; ok {
t.languages[upd.Message.Chat.ID] = sects[1]
for jfID, user := range t.app.storage.telegram {
if user.ChatID == upd.Message.Chat.ID {
user.Lang = sects[1]
t.app.storage.telegram[jfID] = user
if err := t.app.storage.storeTelegramUsers(); err != nil {
t.app.err.Printf("Failed to store Telegram users: %v", err)
}
break
}
}
}
}
func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string) {
tokenIndex := -1
for i, token := range t.tokens {
if upd.Message.Text == token {
tokenIndex = i
break
}
}
if tokenIndex == -1 {
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
return
}
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
t.verifiedTokens = append(t.verifiedTokens, TelegramVerifiedToken{
Token: upd.Message.Text,
ChatID: upd.Message.Chat.ID,
Username: upd.Message.Chat.UserName,
})
t.tokens[len(t.tokens)-1], t.tokens[tokenIndex] = t.tokens[tokenIndex], t.tokens[len(t.tokens)-1]
t.tokens = t.tokens[:len(t.tokens)-1]
}

View File

@@ -1,8 +0,0 @@
module github.com/hrfee/jfa-go/telegram
go 1.16
require (
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
)

View File

@@ -1,4 +0,0 @@
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/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=

View File

@@ -1,59 +0,0 @@
package main
import (
"fmt"
"log"
"strings"
tg "github.com/go-telegram-bot-api/telegram-bot-api"
)
const (
TOKEN = "1785754648:AAG4G6PKZpGDEJM_-MeQHJqD-xUDrrLrTC4"
USER = "johnikwock"
AUTH = "AB-CD-EF"
)
func main() {
log.Println("Connecting...")
bot, err := tg.NewBotAPI(TOKEN)
if err != nil {
log.Fatalf("Failed to initialize bot: %v", err)
}
bot.Debug = false
log.Printf("Authorized Telegram bot \"%s\"", bot.Self.UserName)
u := tg.NewUpdate(0)
u.Timeout = 60
updates, err := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil {
continue
}
log.Printf("New message from \"@%s\": \"%s\"", update.Message.From.UserName, update.Message.Text)
if update.Message.From.UserName != USER {
continue
}
var msg tg.MessageConfig
sects := strings.Split(update.Message.Text, " ")
if sects[0] == "/start" {
msg = tg.NewMessage(update.Message.Chat.ID, fmt.Sprintf("Enter this code on the sign-up page to continue: %s", AUTH))
} else if sects[0] != "/auth" || sects[len(sects)-1] != AUTH {
log.Println("Invalid command or auth token")
msg = tg.NewMessage(update.Message.Chat.ID, "Invalid command or token")
} else {
msg = tg.NewMessage(update.Message.Chat.ID, "Success!")
log.Println("Successful auth")
}
msg.ReplyToMessageID = update.Message.MessageID
_, err := bot.Send(msg)
if err != nil {
log.Printf("Send failed: %v", err)
}
}
}

View File

@@ -1,4 +0,0 @@
API key: 1785754648:AAG4G6PKZpGDEJM_-MeQHJqD-xUDrrLrTC4
Name: jfa-bot
Username: jfago_bot

105
tray.go Normal file
View File

@@ -0,0 +1,105 @@
// +build tray
package main
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/getlantern/systray"
"github.com/skratchdot/open-golang/open"
// "github.com/getlantern/systray"
)
var TRAY = true
func RunTray() {
systray.Run(onReady, onExit)
}
func onExit() {
if RUNNING {
QUIT = true
RESTART <- true
}
os.Remove(SOCK)
}
func onReady() {
icon, err := localFS.ReadFile("web/favicon.ico")
if err != nil {
log.Fatalf("Failed to load favicon: %v", err)
}
systray.SetIcon(icon)
systray.SetTitle("jfa-go")
mStart := systray.AddMenuItem("Start", "Start jfa-go")
mStop := systray.AddMenuItem("Stop", "Stop jfa-go")
mRestart := systray.AddMenuItem("Restart", "Restart jfa-go")
mOpenLogs := systray.AddMenuItem("Open logs", "Open jfa-go log file.")
as := NewAutostart("jfa-go", "A user management system for Jellyfin", "Run on login", "Run jfa-go on user login.")
mQuit := systray.AddMenuItem("Quit", "Quit jfa-go")
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
systray.Quit()
os.Exit(1)
}()
defer func() {
systray.Quit()
}()
RESTART = make(chan bool, 1)
TRAYRESTART = make(chan bool, 1)
go start(false, true)
mStart.Disable()
mStop.Enable()
mRestart.Enable()
go as.HandleCheck()
trayRestart := func() {
if RUNNING {
RESTART <- true
mStop.Disable()
mStart.Enable()
mRestart.Disable()
for {
if !RUNNING {
break
}
}
go start(false, false)
mStart.Disable()
mStop.Enable()
mRestart.Enable()
}
}
for {
select {
case <-mStart.ClickedCh:
if !RUNNING {
go start(false, false)
mStart.Disable()
mStop.Enable()
mRestart.Enable()
}
case <-mStop.ClickedCh:
if RUNNING {
RESTART <- true
mStop.Disable()
mStart.Enable()
mRestart.Disable()
}
case <-TRAYRESTART:
trayRestart()
case <-mRestart.ClickedCh:
trayRestart()
case <-mOpenLogs.ClickedCh:
open.Start(logPath)
case <-mQuit.ClickedCh:
systray.Quit()
}
}
}

View File

@@ -63,9 +63,15 @@ window.availableProfiles = window.availableProfiles || [];
window.modals.updateInfo = new Modal(document.getElementById("modal-update"));
window.modals.matrix = new Modal(document.getElementById("modal-matrix"));
if (window.telegramEnabled) {
window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
}
if (window.discordEnabled) {
window.modals.discord = new Modal(document.getElementById("modal-discord"));
}
})();
var inviteCreator = new createInvite();

39
ts/crash.ts Normal file
View File

@@ -0,0 +1,39 @@
import { toClipboard } from "./modules/common.js";
const buttonNormal = document.getElementById("button-log-normal") as HTMLInputElement;
const buttonSanitized = document.getElementById("button-log-sanitized") as HTMLInputElement;
const logNormal = document.getElementById("log-normal") as HTMLInputElement;
const logSanitized = document.getElementById("log-sanitized") as HTMLInputElement;
const buttonChange = (type: string) => {
console.log("RUN");
if (type == "normal") {
logSanitized.classList.add("unfocused");
logNormal.classList.remove("unfocused");
buttonNormal.classList.add("!high");
buttonNormal.classList.remove("!normal");
buttonSanitized.classList.add("!normal");
buttonSanitized.classList.remove("!high");
} else {
logNormal.classList.add("unfocused");
logSanitized.classList.remove("unfocused");
buttonSanitized.classList.add("!high");
buttonSanitized.classList.remove("!normal");
buttonNormal.classList.add("!normal");
buttonNormal.classList.remove("!high");
}
}
buttonNormal.onclick = () => buttonChange("normal");
buttonSanitized.onclick = () => buttonChange("sanitized");
const copyButton = document.getElementById("copy-log") as HTMLSpanElement;
copyButton.onclick = () => {
if (logSanitized.classList.contains("unfocused")) {
toClipboard("```\n" + logNormal.textContent + "```");
} else {
toClipboard("```\n" + logSanitized.textContent + "```");
}
copyButton.textContent = "Copied.";
setTimeout(() => { copyButton.textContent = "Copy"; }, 1500);
};

View File

@@ -1,19 +1,28 @@
import { Modal } from "./modules/modal.js";
import { notificationBox, whichAnimationEvent } from "./modules/common.js";
import { _get, _post, toggleLoader, toDateString } from "./modules/common.js";
import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString } from "./modules/common.js";
import { loadLangSelector } from "./modules/lang.js";
import { initValidator } from "./modules/validator.js";
interface formWindow extends Window {
validationStrings: pwValStrings;
invalidPassword: string;
successModal: Modal;
telegramModal: Modal;
discordModal: Modal;
matrixModal: Modal;
confirmationModal: Modal
code: string;
messages: { [key: string]: string };
confirmation: boolean;
telegramRequired: boolean;
telegramPIN: string;
discordRequired: boolean;
discordPIN: string;
discordStartCommand: string;
discordInviteLink: boolean;
discordServerName: string;
matrixRequired: boolean;
matrixUserID: string;
userExpiryEnabled: boolean;
userExpiryMonths: number;
userExpiryDays: number;
@@ -22,20 +31,6 @@ interface formWindow extends Window {
userExpiryMessage: string;
}
interface pwValString {
singular: string;
plural: string;
}
interface pwValStrings {
length: pwValString;
uppercase: pwValString;
lowercase: pwValString;
number: pwValString;
special: pwValString;
[ type: string ]: pwValString;
}
loadLangSelector("form");
window.notifications = new notificationBox(document.getElementById("notification-box") as HTMLDivElement);
@@ -68,7 +63,7 @@ if (window.telegramEnabled) {
telegramVerified = true;
waiting.classList.add("~positive");
waiting.classList.remove("~info");
window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]);
window.notifications.customPositive("telegramVerified", "", window.messages["verified"]);
setTimeout(window.telegramModal.close, 2000);
telegramButton.classList.add("unfocused");
document.getElementById("contact-via").classList.remove("unfocused");
@@ -84,6 +79,129 @@ if (window.telegramEnabled) {
};
}
interface DiscordInvite {
invite: string;
icon: string;
}
var discordVerified = false;
if (window.discordEnabled) {
window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired);
const discordButton = document.getElementById("link-discord") as HTMLSpanElement;
if (window.discordInviteLink) {
_get("/invite/" + window.code + "/discord/invite", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
return;
}
const inv = req.response as DiscordInvite;
const link = document.getElementById("discord-invite") as HTMLAnchorElement;
link.classList.add("subheading", "link-center");
link.href = inv.invite;
link.target = "_blank";
link.innerHTML = `<span class="img-circle lg mr-1"><img class="img-circle" src="${inv.icon}" width="64" height="64"></span>${window.discordServerName}`;
}
});
}
discordButton.onclick = () => {
const waiting = document.getElementById("discord-waiting") as HTMLSpanElement;
toggleLoader(waiting);
window.discordModal.show();
let modalClosed = false;
window.discordModal.onclose = () => {
modalClosed = true;
toggleLoader(waiting);
}
const checkVerified = () => _get("/invite/" + window.code + "/discord/verified/" + window.discordPIN, null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 401) {
window.discordModal.close();
window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]);
return;
} else if (req.status == 200) {
if (req.response["success"] as boolean) {
discordVerified = true;
waiting.classList.add("~positive");
waiting.classList.remove("~info");
window.notifications.customPositive("discordVerified", "", window.messages["verified"]);
setTimeout(window.discordModal.close, 2000);
discordButton.classList.add("unfocused");
document.getElementById("contact-via").classList.remove("unfocused");
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
radio.checked = true;
} else if (!modalClosed) {
setTimeout(checkVerified, 1500);
}
}
}
});
checkVerified();
};
}
var matrixVerified = false;
var matrixPIN = "";
if (window.matrixEnabled) {
window.matrixModal = new Modal(document.getElementById("modal-matrix"), window.matrixRequired);
const matrixButton = document.getElementById("link-matrix") as HTMLSpanElement;
matrixButton.onclick = window.matrixModal.show;
const submitButton = document.getElementById("matrix-send") as HTMLSpanElement;
const input = document.getElementById("matrix-userid") as HTMLInputElement;
let userID = "";
submitButton.onclick = () => {
addLoader(submitButton);
if (userID == "") {
const send = {
user_id: input.value
};
_post("/invite/" + window.code + "/matrix/user", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
removeLoader(submitButton);
userID = input.value;
if (req.status != 200) {
window.notifications.customError("errorUnknown", window.messages["errorUnknown"]);
window.matrixModal.close();
return;
}
submitButton.classList.add("~positive");
submitButton.classList.remove("~info");
setTimeout(() => {
submitButton.classList.add("~info");
submitButton.classList.remove("~positive");
}, 2000);
input.placeholder = "PIN";
input.value = "";
}
});
} else {
_get("/invite/" + window.code + "/matrix/verified/" + userID + "/" + input.value, null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
removeLoader(submitButton)
const valid = req.response["success"] as boolean;
if (valid) {
window.matrixModal.close();
window.notifications.customPositive("successVerified", "", window.messages["verified"]);
matrixVerified = true;
matrixPIN = input.value;
matrixButton.classList.add("unfocused");
document.getElementById("contact-via").classList.remove("unfocused");
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
radio.checked = true;
} else {
window.notifications.customError("errorInvalidPIN", window.messages["errorInvalidPIN"]);
submitButton.classList.add("~critical");
submitButton.classList.remove("~info");
setTimeout(() => {
submitButton.classList.add("~info");
submitButton.classList.remove("~critical");
}, 800);
}
}
},);
}
};
}
if (window.confirmation) {
window.confirmationModal = new Modal(document.getElementById("modal-confirmation"), true);
}
@@ -103,29 +221,6 @@ if (window.userExpiryEnabled) {
calculateTime();
}
var defaultPwValStrings: pwValStrings = {
length: {
singular: "Must have at least {n} character",
plural: "Must have at least {n} characters"
},
uppercase: {
singular: "Must have at least {n} uppercase character",
plural: "Must have at least {n} uppercase characters"
},
lowercase: {
singular: "Must have at least {n} lowercase character",
plural: "Must have at least {n} lowercase characters"
},
number: {
singular: "Must have at least {n} number",
plural: "Must have at least {n} numbers"
},
special: {
singular: "Must have at least {n} special character",
plural: "Must have at least {n} special characters"
}
}
const form = document.getElementById("form-create") as HTMLFormElement;
const submitButton = form.querySelector("input[type=submit]") as HTMLInputElement;
const submitSpan = form.querySelector("span.submit") as HTMLSpanElement;
@@ -135,19 +230,7 @@ if (!window.usernameEnabled) { usernameField.parentElement.remove(); usernameFie
const passwordField = document.getElementById("create-password") as HTMLInputElement;
const rePasswordField = document.getElementById("create-reenter-password") as HTMLInputElement;
const checkPasswords = () => {
if (passwordField.value != rePasswordField.value) {
rePasswordField.setCustomValidity(window.invalidPassword);
submitButton.disabled = true;
submitSpan.setAttribute("disabled", "");
} else {
rePasswordField.setCustomValidity("");
submitButton.disabled = false;
submitSpan.removeAttribute("disabled");
}
};
rePasswordField.addEventListener("keyup", checkPasswords);
passwordField.addEventListener("keyup", checkPasswords);
var requirements = initValidator(passwordField, rePasswordField, submitButton, submitSpan)
interface respDTO {
response: boolean;
@@ -161,6 +244,10 @@ interface sendDTO {
password: string;
telegram_pin?: string;
telegram_contact?: boolean;
discord_pin?: string;
discord_contact?: boolean;
matrix_pin?: string;
matrix_contact?: boolean;
}
const create = (event: SubmitEvent) => {
@@ -179,6 +266,20 @@ const create = (event: SubmitEvent) => {
send.telegram_contact = true;
}
}
if (discordVerified) {
send.discord_pin = window.discordPIN;
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
if (radio.checked) {
send.discord_contact = true;
}
}
if (matrixVerified) {
send.matrix_pin = matrixPIN;
const radio = document.getElementById("contact-via-matrix") as HTMLInputElement;
if (radio.checked) {
send.matrix_contact = true;
}
}
_post("/newUser", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
let vals = req.response as respDTO;
@@ -221,96 +322,3 @@ const create = (event: SubmitEvent) => {
};
form.onsubmit = create;
class Requirement {
private _name: string;
protected _minCount: number;
private _content: HTMLSpanElement;
private _valid: HTMLSpanElement;
private _li: HTMLLIElement;
get valid(): boolean { return this._valid.classList.contains("~positive"); }
set valid(state: boolean) {
if (state) {
this._valid.classList.add("~positive");
this._valid.classList.remove("~critical");
this._valid.innerHTML = `<i class="icon ri-check-line" title="valid"></i>`;
} else {
this._valid.classList.add("~critical");
this._valid.classList.remove("~positive");
this._valid.innerHTML = `<i class="icon ri-close-line" title="invalid"></i>`;
}
}
constructor(name: string, el: HTMLLIElement) {
this._name = name;
this._li = el;
this._content = this._li.querySelector("span.requirement-content") as HTMLSpanElement;
this._valid = this._li.querySelector("span.requirement-valid") as HTMLSpanElement;
this.valid = false;
this._minCount = +this._li.getAttribute("min");
let text = "";
if (this._minCount == 1) {
text = window.validationStrings[this._name].singular.replace("{n}", "1");
} else {
text = window.validationStrings[this._name].plural.replace("{n}", ""+this._minCount);
}
this._content.textContent = text;
}
validate = (count: number) => { this.valid = (count >= this._minCount); }
}
// Incredible code right here
const isInt = (s: string): boolean => { return (s in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]); }
const testStrings = (f: pwValString): boolean => {
const testString = (s: string): boolean => {
if (s == "" || !s.includes("{n}")) { return false; }
return true;
}
return testString(f.singular) && testString(f.plural);
}
interface Validation { [name: string]: number }
const validate = (s: string): Validation => {
let v: Validation = {};
for (let criteria of ["length", "lowercase", "uppercase", "number", "special"]) { v[criteria] = 0; }
v["length"] = s.length;
for (let c of s) {
if (isInt(c)) { v["number"]++; }
else {
const upper = c.toUpperCase();
if (upper == c.toLowerCase()) { v["special"]++; }
else {
if (upper == c) { v["uppercase"]++; }
else if (upper != c) { v["lowercase"]++; }
}
}
}
return v
}
passwordField.addEventListener("keyup", () => {
const v = validate(passwordField.value);
for (let criteria in requirements) {
requirements[criteria].validate(v[criteria]);
}
});
var requirements: { [category: string]: Requirement} = {};
if (!window.validationStrings) {
window.validationStrings = defaultPwValStrings;
} else {
for (let category in window.validationStrings) {
if (!testStrings(window.validationStrings[category])) {
window.validationStrings[category] = defaultPwValStrings[category];
}
const el = document.getElementById("requirement-" + category);
if (el) {
requirements[category] = new Requirement(category, el as HTMLLIElement);
}
}
}

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