Compare commits

...

51 Commits
0.6.2 ... 0.7.0

Author SHA1 Message Date
James Mills dd1bc41b75
Update CHANGELOG for 0.7.0 2 months ago
James Mills 6d0ae86cca
Fix bug in setting tokens for passwrod reset and magiclink auth 2 months ago
James Mills be08a7d3ef
Fix minor bug in TTLCache.get() 2 months ago
James Mills 59bc1ff8cf
Fix cached tokens to be deleted after use 2 months ago
James Mills 2b0dec2604
Add improved UX for Follow/Unfollow and Mute/Unmute with graceful JS fallback 2 months ago
James Mills 9c155411bb
Fix token expiry check on password reset form 2 months ago
James Mills 5dec1181ca
Fix password reset and magiclink auth tokens to only be usable once with a default TTL of 30m 2 months ago
James Mills ffa4cee1fb
Remove now unused separate linux and windows GoReleaser configs 2 months ago
James Mills b201fecd19
Delete newpost binary (how dafuq did that get there?\!) 2 months ago
James Mills df04e9b13b
Fix edit/reply/fork buttons ot use ID selectors 2 months ago
James Mills 6a0f37f037
Update deps 2 months ago
James Mills 872ac1f6be
Fix /custom static route for prod builds 2 months ago
James Mills 1f9435d4ff
Add support for Pod-level (User overridable) Timezone, Time and external link preferences 2 months ago
James Mills 939a287907
Add support for /custom/*filepath arbitrary static file serving from a theme with any directory/file structure 2 months ago
James Mills e4bcc518a6
Fix incorrect cache header used in conversation view 3 months ago
James Mills 894fd2a7f9
Fix FeatureFlags' JSON marshalling/unmarshalling 3 months ago
James Mills 24385faf15
Fix Cached.UpdateFeed() to not overwrite a cached feed with an empty set of twts 3 months ago
James Mills b060a55d4f
Fix misspelled translation key for ErrTokenExpired 3 months ago
James Mills 8bc6f91df4
Fix autocomplete for Login via EMail's username field 3 months ago
James Mills 865e184715
Ooops fix a rendering bug 3 months ago
James Mills fa78748efd
Add missing lang msg for MsgMagicLinkAuthEmailSent 3 months ago
James Mills 6c1423abce
Add support for Login via Email with feature magic_link_auth 3 months ago
James Mills 84cfd6bdcd
Update contributing guidelines 3 months ago
James Mills ddb7095731
Add isFeatureEnabled() func to templates 3 months ago
James Mills e41a696cf4
Remove unused Token model 3 months ago
James Mills 1b25d80f68
Cleanup a lot of logging and other general code cleanup 3 months ago
James Mills 63dde37e59
Revert back to the old Cache behaviour of overwriting cached feeds 3 months ago
James Mills 4dd69dc2f6
Add support for custom pages (Fixes #393) 3 months ago
James Mills f628e3fe2e
Add s simple JS confirm to logout to avoid accidental logout 3 months ago
James Mills e2a2913bad
Fix potential nil pointer bug in PermalinkHandler() 3 months ago
James Mills b80bfcee96
Fix bug that logs pod owner out when deleting users 3 months ago
James Mills 793a9c02f4
Add a PruneUsers job that runs once per week with an email list of candidate users to delete for the Pod Owner based on some heuristics 3 months ago
marado 86938cac92 Removed warning regarding #250 (#516) 3 months ago
James Mills edc383424a
Refactor feed handling, fixed a few bugs and allow pod admins to manage @stats and @twtxt feeds too 3 months ago
James Mills 7fb3eb8d46
Refactor feeds handlers into their own source file 3 months ago
James Mills 1398661ee2
Fix the session cookie name once and for all 🥳 3 months ago
James Mills 011c704776
Refacor webmention handling of mentioned Twters 3 months ago
James Mills 03504d9d9e
Do the same for in debug mode (-D) 3 months ago
James Mills dff44afa02
Fix Pod Base URL automatically if it's missing a Scheme, log a warning and assume https:// 3 months ago
James Mills 1482f9e03e
Add new pod yarn.meff.me to manually track versions to help keep the ecosystem up-to-date as we grow :D 3 months ago
James Mills a80bdda790
Add basic validation of the Pod's Base URL to ensure it is TLS enabled in non-debug (production) mode 3 months ago
movq e460caa8f1 Add pagination extension spec (#494) 3 months ago
xuu 3882134ebe fix: make date parse more exact (#514) 3 months ago
James Mills 15dbcee6dd
Fix caching bug with editing and deleting last twt 3 months ago
James Mills 3394aa4734
Fix default MediaResolution to match default themes page width of 850px (Fixes #508) 3 months ago
James Mills 59bfe22638
Code cleanup 3 months ago
James Mills 77161bf676
Fix Cache.FetchTwts() to Normalize Feed.URL(s) before doing anything 3 months ago
James Mills ada2b3aaed
Fix Session Cookie name to by config.LocalURL().Hostname() so the Pod name can be more free form 3 months ago
James Mills 6160c5d59c
Fix NormalizeURL() to strip URL Fragments from Feed URIs 3 months ago
James Mills 90d7cff298
Fix short time ago formats (Fixes #509) 3 months ago
James Mills 36a361ebac
Fix title entries of Atom/XML feeds 3 months ago
  1. 34
      .goreleaser-darwin.yml
  2. 35
      .goreleaser-linux.yml
  3. 54
      CHANGELOG.md
  4. 18
      CONTRIBUTING.md
  5. 4
      README.md
  6. 0
      data/pages/.gitkeep
  7. 5
      docs/_posts/2021-10-09-metadataextension.md
  8. 74
      docs/_posts/2021-10-30-archivefeedsextension.md
  9. 13
      go.mod
  10. 12
      go.sum
  11. 31
      internal/api.go
  12. 2
      internal/archive.go
  13. 60
      internal/bitcask_store.go
  14. 62
      internal/cache.go
  15. 24
      internal/config.go
  16. 20
      internal/context.go
  17. 1
      internal/conversation_handler.go
  18. 119
      internal/email.go
  19. 10
      internal/external_handlers.go
  20. 35
      internal/features.go
  21. 372
      internal/feeds_handlers.go
  22. 920
      internal/handlers.go
  23. 115
      internal/jobs.go
  24. 15
      internal/langs/active.en.toml
  25. 24
      internal/langs/active.zh-CN.toml
  26. 24
      internal/langs/active.zh-TW.toml
  27. 265
      internal/login_handlers.go
  28. 15
      internal/logout_handler.go
  29. 11
      internal/manage_handlers.go
  30. 4
      internal/media_handlers.go
  31. 50
      internal/models.go
  32. 63
      internal/options.go
  33. 109
      internal/page_handlers.go
  34. 262
      internal/password_handlers.go
  35. 2
      internal/passwords/scrypt_passwords.go
  36. 2
      internal/permalink_handler.go
  37. 106
      internal/register_handler.go
  38. 32
      internal/server.go
  39. 115
      internal/settings_handlers.go
  40. 6
      internal/store.go
  41. 7
      internal/support_handlers.go
  42. 20
      internal/templates.go
  43. 0
      internal/theme/static/custom/.gitkeep
  44. 60
      internal/theme/static/js/99-yarn.js
  45. 2
      internal/theme/templates/base.html
  46. 14
      internal/theme/templates/login.html
  47. 26
      internal/theme/templates/login_email.html
  48. 46
      internal/theme/templates/manageFeed.html
  49. 42
      internal/theme/templates/managePod.html
  50. 14
      internal/theme/templates/partials.html
  51. 42
      internal/theme/templates/profile.html
  52. 2
      internal/theme/templates/register.html
  53. 37
      internal/theme/templates/settings.html
  54. 5
      internal/tokencache.go
  55. 9
      internal/ttlcache.go
  56. 19
      internal/twtxt_handlers.go
  57. 51
      internal/utils.go
  58. 4
      internal/utils_test.go
  59. 13
      internal/webmention/webmention.go
  60. 1
      internal/whofollows_handler.go
  61. 3
      newpost
  62. 2
      tools/check-pod-versions.sh
  63. 4
      types/lextwt/lextwt.go
  64. 3
      types/lextwt/lextwt_test.go
  65. 44
      types/lextwt/parser.go

34
.goreleaser-darwin.yml

@ -1,34 +0,0 @@
---
builds:
- id: twt
binary: twt
main: ./cmd/twt
flags: -tags "static_build"
ldflags: >-
-w
-X git.mills.io/yarnsocial/yarn.Version={{.Version}}
-X git.mills.io/yarnsocial/yarn.Commit={{.Commit}}
env:
- CGO_ENABLED=0
goos:
- darwin
- windows
goarch:
- amd64
signs:
- artifacts: checksum
brews:
-
tap:
owner: prologic
name: homebrew-twtxt
homepage: "https://github.io/jointwt/twtxt"
description: |
📕 twtxt is a Self-Hosted, Twitter™-like Decentralised microBlogging
platform. No ads, no tracking, your content, your data!
release:
github:
owner: prologic
name: twtxt
draft: true

35
.goreleaser-linux.yml

@ -1,35 +0,0 @@
---
builds:
- id: twt
binary: twt
main: ./cmd/twt
flags: -tags "static_build"
ldflags: >-
-w
-X git.mills.io/yarnsocial/yarn.Version={{.Version}}
-X git.mills.io/yarnsocial/yarn.Commit={{.Commit}}
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
goarm:
- 6
- 7
signs:
- artifacts: checksum
brews:
-
tap:
owner: prologic
name: homebrew-twtxt
homepage: "https://github.io/jointwt/twtxt"
description: |
📕 twtxt is a Self-Hosted, Twitter™-like Decentralised microBlogging
platform. No ads, no tracking, your content, your data!
release:
github:
owner: prologic
name: twtxt
draft: true

54
CHANGELOG.md

@ -1,4 +1,54 @@
<a name="0.7.0"></a>
## [0.7.0](https://git.mills.io/yarnsocial/yarn/compare/0.6.2...0.7.0) (2021-11-13)
### Bug Fixes
* Fix bug in setting tokens for passwrod reset and magiclink auth
* Fix minor bug in TTLCache.get()
* Fix cached tokens to be deleted after use
* Fix token expiry check on password reset form
* Fix password reset and magiclink auth tokens to only be usable once with a default TTL of 30m
* Fix edit/reply/fork buttons ot use ID selectors
* Fix /custom static route for prod builds
* Fix incorrect cache header used in conversation view
* Fix FeatureFlags' JSON marshalling/unmarshalling
* Fix Cached.UpdateFeed() to not overwrite a cached feed with an empty set of twts
* Fix misspelled translation key for ErrTokenExpired
* Fix autocomplete for Login via EMail's username field
* Fix potential nil pointer bug in PermalinkHandler()
* Fix bug that logs pod owner out when deleting users
* Fix the session cookie name once and for all 🥳
* Fix Pod Base URL automatically if it's missing a Scheme, log a warning and assume https://
* Fix caching bug with editing and deleting last twt
* Fix default MediaResolution to match default themes page width of 850px (Fixes #508)
* Fix Cache.FetchTwts() to Normalize Feed.URL(s) before doing anything
* Fix Session Cookie name to by config.LocalURL().Hostname() so the Pod name can be more free form
* Fix NormalizeURL() to strip URL Fragments from Feed URIs
* Fix short time ago formats (Fixes #509)
* Fix title entries of Atom/XML feeds
### Features
* Add improved UX for Follow/Unfollow and Mute/Unmute with graceful JS fallback
* Add support for Pod-level (User overridable) Timezone, Time and external link preferences
* Add support for /custom/*filepath arbitrary static file serving from a theme with any directory/file structure
* Add missing lang msg for MsgMagicLinkAuthEmailSent
* Add support for Login via Email with feature magic_link_auth
* Add isFeatureEnabled() func to templates
* Add support for custom pages (Fixes #393)
* Add s simple JS confirm to logout to avoid accidental logout
* Add a PruneUsers job that runs once per week with an email list of candidate users to delete for the Pod Owner based on some heuristics
* Add new pod yarn.meff.me to manually track versions to help keep the ecosystem up-to-date as we grow :D
* Add basic validation of the Pod's Base URL to ensure it is TLS enabled in non-debug (production) mode
* Add pagination extension spec (#494)
### Updates
* Update deps
* Update contributing guidelines
<a name="0.6.2"></a>
## [0.6.2](https://git.mills.io/yarnsocial/yarn/compare/0.6.1...0.6.2) (2021-11-08)
@ -14,6 +64,10 @@
* Add article target (#506)
* Add per-handler latency metrics
### Updates
* Update CHANGELOG for 0.6.2
<a name="0.6.1"></a>
## [0.6.1](https://git.mills.io/yarnsocial/yarn/compare/0.6.0...0.6.1) (2021-11-06)

18
CONTRIBUTING.md

@ -1,13 +1,15 @@
# Contributing
No preference. If you know how to use Github and have contributed to open source projects before then:
This project is hosted on a self-hosted [Gitea](https://gitea.io/en-us/)
instance. If you know how to use Github and have contributed to open source
projects before then, you'll feel right at home. Github OAuth is also enabled
so you can easily create an account and start contributing right away:
* File an issue
* Submit a pull request
* File an issue + Submit a pull request
* Use this project somewhere :)
- File an issue
- Submit a pull request
- File an issue + Submit a pull request
- Use this project somewhere :)
Be sure to add yourself to the [AUTHORS](/AUTHORS)
file when you submit your PR(s). Every contribution counts no how big or small!
Be sure to add yourself to the [AUTHORS](./AUTHORS) file when you submit your PR(s). Every contribution counts no how big or small!
Thanks for using twtxt!
Thanks for using this project!

4
README.md

@ -13,10 +13,6 @@ See [Yarn.social] for more deatils
### Pre-built Binaries
__NOTE:__ Please don't use the pre-built binaries until [Issue #250](https://git.mills.io/yarnsocial/yarn/issues/250) is resolved.
Please build from source or use the [Docker Images](https://hub.docker.com/jointwt).
Thank you. 🙇‍♂️
As a first point, please try to use one of the pre-built binaries that are
available on the [Releases](https://git.mills.io/yarnsocial/yarn/releases) page.

0
data/pages/.gitkeep

5
docs/_posts/2021-10-09-metadataextension.md

@ -143,6 +143,11 @@ whitespace.
# link = All my source code https://git.example.com/
```
### `prev`
This field is used by the [Archive Feeds
Extension](archivefeedsextension.html).
## Changelog
* 2021-10-09: Initial version.

74
docs/_posts/2021-10-30-archivefeedsextension.md

@ -0,0 +1,74 @@
---
layout: page
title: "Archive Feeds Extension"
category: doc
date: 2021-10-30 11:00:00
order: 3
---
At [twtxt.net](https://twtxt.net/) **Archive Feeds** were invented as an
extension to the original [Twtxt File Format
Specification](https://twtxt.readthedocs.io/en/latest/user/twtxtfile.html#format-specification).
## Purpose
Feeds grow over time. To avoid feeds of virtually unlimited size,
pagination can be used to move old twts to a different (partial) feed.
Clients can then choose to retrieve only some of those feeds.
## Main Feed and Archived Feeds
There is exactly one main feed, which is the same as the traditional
twtxt.txt file. This feed keeps growing by adding new twts at the end
(this differs from the original twtxt spec, which allowed adding new
twts anywhere in the feed). Deletion or editing of twts anywhere in the
feed is allowed.
Once the main feed is "full", some or all of its twts can be moved to a
different feed: an archived feed. There can be any number of archived
feeds. Once they are made public, they are supposed to be left alone and
won't receive further updates. Deletion or editing is still allowed, but
feed authors should not expect clients to retrieve archived feeds on a
regular basis (or at all). When moving twts to an archived feed, their
relative order should be retained. A twt should only appear in one feed,
either the main feed or an archived feed, but not in both.
A feed's author decides when a feed is "full" and should be archived.
For example, this can be based on the number of twts in the feed or it
can be based on date ranges.
The main feed and all archived feeds form a linked list using the
[metadata](metadataextension.html) field described below.
## Format
The main feed can contain a [metadata](metadataextension.html) field
called `prev` which points to the URL of an archived feed (i.e., it
contains *older* twts):
```
# url = https://example.com/twtxt.txt
# url = gopher://example.com/0/twtxt.txt
# nick = cathy
# prev = kpw257a twtxt-2021-10-18.txt
```
The file names of archived feeds are implementation specific and don't
carry special meaning.
Archived feeds *can* contain another `prev` field to point to yet
another archived feed.
The first value of `prev` is the [twt hash](twthashextension.html) of
the last twt in that feed. It is provided as a hint for clients.
The second value of `prev` is a name relative to the base directory of
the feed's URL in `url` (more specifically, in the URL that the client
used to retrieve the feed). In the example above, `prev` would evaluate
to the full URL <https://example.com/twtxt-2021-10-18.txt> for HTTPS and
<gopher://example.com/0/twtxt-2021-10-18.txt> for Gopher.
For all feeds (main and archived), the `url` fields of the main feed
shall be used for [twt hashing](twthashextension.html). (There can be
multiple `url` fields in the main feed, see [the page metadata
extension](metadataextension.html) on how to select the correct one.)

13
go.mod

@ -27,7 +27,7 @@ require (
github.com/go-mail/mail v2.3.1+incompatible
github.com/goccy/go-yaml v1.9.4
github.com/gofrs/flock v0.8.1 // indirect
github.com/gomarkdown/markdown v0.0.0-20210918233619-6c1113f12c4a
github.com/gomarkdown/markdown v0.0.0-20211105120026-16f708f914c3
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/feeds v1.1.1
github.com/goware/urlx v0.3.1
@ -65,7 +65,7 @@ require (
github.com/securisec/go-keywords v0.0.0-20200619134240-769e7273f2ed
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sirupsen/logrus v1.8.1
github.com/slok/go-http-metrics v0.9.0 // indirect
github.com/slok/go-http-metrics v0.9.0
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.9.0
@ -78,14 +78,15 @@ require (
github.com/vcraescu/go-paginator v1.0.0
github.com/wblakecaldwell/profiler v0.0.0-20150908040756-6111ef1313a1
github.com/writeas/slug v1.2.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/exp v0.0.0-20211029182501-9b944d235b9d // indirect
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
golang.org/x/exp v0.0.0-20211111183329-cb5df436b1a8 // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
golang.org/x/net v0.0.0-20211029224645-99673261e6eb
golang.org/x/sys v0.0.0-20211031064116-611d5d643895 // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
golang.org/x/sys v0.0.0-20211112193437-faf0a1b62c6b // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/text v0.3.7
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.64.0 // indirect
gopkg.in/mail.v2 v2.3.1 // indirect
gopkg.in/yaml.v2 v2.4.0
gorm.io/gorm v1.22.2 // indirect

12
go.sum

@ -292,6 +292,8 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20210918233619-6c1113f12c4a h1:syEwbl3pF5Y1mnIStrPwqd50vNU1AAKuAy8HFCPAgUc=
github.com/gomarkdown/markdown v0.0.0-20210918233619-6c1113f12c4a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gomarkdown/markdown v0.0.0-20211105120026-16f708f914c3 h1:YYPIb87JCyLEK3Iw/dfgmpLm2cL2Qc7JNNdYdE87b9s=
github.com/gomarkdown/markdown v0.0.0-20211105120026-16f708f914c3/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -801,6 +803,8 @@ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -815,6 +819,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/exp v0.0.0-20200228211341-fcea875c7e85/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/exp v0.0.0-20211029182501-9b944d235b9d h1:MKwb3mzSy4CTpgAm10+7Ru8Hq8ZnHOGgWAjo9fNVK+o=
golang.org/x/exp v0.0.0-20211029182501-9b944d235b9d/go.mod h1:OyI624f2tQ/aU3IMa7GB16Hk54CHURAfHfj6tMqtyhA=
golang.org/x/exp v0.0.0-20211111183329-cb5df436b1a8 h1:FJS8Pv3WYbMBIQqVzFH33DT5AuGjULOduxuHS5zlv+A=
golang.org/x/exp v0.0.0-20211111183329-cb5df436b1a8/go.mod h1:OyI624f2tQ/aU3IMa7GB16Hk54CHURAfHfj6tMqtyhA=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
@ -899,6 +905,8 @@ golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1003,6 +1011,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211031064116-611d5d643895 h1:iaNpwpnrgL5jzWS0vCNnfa8HqzxveCFpFx3uC/X4Tps=
golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211112193437-faf0a1b62c6b h1:uo+9AuR+gDt/gdj+1BaLhdOHsaGI6YU6585IiDcLrFE=
golang.org/x/sys v0.0.0-20211112193437-faf0a1b62c6b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -1239,6 +1249,8 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c=
gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.64.0 h1:Mj2zXEXcNb5joEiSA0zc3HZpTst/iyjNiR4CN8tDzOg=
gopkg.in/ini.v1 v1.64.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=

31
internal/api.go

@ -41,6 +41,15 @@ var (
ErrInvalidToken = errors.New("error: invalid token")
)
// Token ...
type Token struct {
Signature string
Value string
UserAgent string
CreatedAt time.Time
ExpiresAt time.Time
}
// API ...
type API struct {
router *Router
@ -276,8 +285,6 @@ func (a *API) RegisterEndpoint() httprouter.Handle {
http.Error(w, "User Creation Failed", http.StatusInternalServerError)
return
}
log.Infof("user registered: %v", user)
}
}
@ -299,7 +306,6 @@ func (a *API) AuthEndpoint() httprouter.Handle {
// Error: no username or password provided
if username == "" || password == "" {
log.Warn("no username or password provided")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
@ -343,18 +349,6 @@ func (a *API) AuthEndpoint() httprouter.Handle {
return
}
user.AddToken(token)
if err := a.db.SetToken(token.Signature, token); err != nil {
log.WithError(err).Error("error saving token object")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if err := a.db.SetUser(user.Username, user); err != nil {
log.WithError(err).Error("error saving user object")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
res := types.AuthResponse{Token: token.Value}
body, err := res.Bytes()
@ -383,7 +377,6 @@ func (a *API) PostEndpoint() httprouter.Handle {
text := CleanTwt(req.Text)
if text == "" {
log.Warn("no text provided for post")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
@ -587,7 +580,6 @@ func (a *API) FollowEndpoint() httprouter.Handle {
url := NormalizeURL(req.URL)
if nick == "" || url == "" {
log.Warn("no nick or url provided")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
@ -691,7 +683,6 @@ func (a *API) UnfollowEndpoint() httprouter.Handle {
nick := req.Nick
if nick == "" {
log.Error("no nick provided")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
@ -704,7 +695,6 @@ func (a *API) UnfollowEndpoint() httprouter.Handle {
url, ok := user.Following[nick]
if !ok {
log.Errorf("user %s is not following %s", nick, url)
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
@ -897,7 +887,6 @@ func (a *API) UploadMediaEndpoint() httprouter.Handle {
mfile, headers, err := r.FormFile("media_file")
if err != nil && err != http.ErrMissingFile {
if err.Error() == "http: request body too large" {
log.Warnf("request too large for media upload from %s", FormatRequest(r))
http.Error(w, "Media Upload Too Large", http.StatusRequestEntityTooLarge)
return
}
@ -907,7 +896,6 @@ func (a *API) UploadMediaEndpoint() httprouter.Handle {
}
if mfile == nil || headers == nil {
log.Warn("no valid media file uploaded")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
@ -971,7 +959,6 @@ func (a *API) UploadMediaEndpoint() httprouter.Handle {
}
if uri.IsZero() {
log.Warn("no media file provided or unsupported media type")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}

2
internal/archive.go

@ -123,7 +123,6 @@ func (a *DiskArchiver) Get(hash string) (types.Twt, error) {
}
if !a.fileExists(fn) {
log.Warnf("twt %s not found in archive", hash)
return types.NilTwt, ErrTwtNotArchived
}
@ -150,7 +149,6 @@ func (a *DiskArchiver) Archive(twt types.Twt) error {
}
if a.fileExists(fn) {
log.Warnf("archived twt %s already exists", twt.Hash())
return ErrTwtAlreadyArchived
}

60
internal/bitcask_store.go

@ -17,7 +17,6 @@ const (
feedsKeyPrefix = "/feeds"
sessionsKeyPrefix = "/sessions"
usersKeyPrefix = "/users"
tokensKeyPrefix = "/tokens"
)
// BitcaskStore ...
@ -330,62 +329,3 @@ func (bs *BitcaskStore) GetAllSessions() ([]*session.Session, error) {
return sessions, nil
}
func (bs *BitcaskStore) GetUserTokens(user *User) ([]*Token, error) {
tokens := []*Token{}
for _, signature := range user.Tokens {
tkn, err := bs.GetToken(signature)
if err != nil {
return tokens, err
}
tokens = append(tokens, tkn)
}
return tokens, nil
}
func (bs *BitcaskStore) GetToken(signature string) (*Token, error) {
key := []byte(fmt.Sprintf("%s/%s", tokensKeyPrefix, signature))
data, err := bs.db.Get(key)
if err == bitcask.ErrKeyNotFound {
return nil, ErrTokenNotFound
}
tkn, err := LoadToken(data)
if err != nil {
return nil, err
}
return tkn, nil
}
func (bs *BitcaskStore) SetToken(signature string, tkn *Token) error {
data, err := tkn.Bytes()
if err != nil {
return err
}
key := []byte(fmt.Sprintf("%s/%s", tokensKeyPrefix, signature))
if err := bs.db.Put(key, data); err != nil {
return err
}
return nil
}
func (bs *BitcaskStore) DelToken(signature string) error {
key := []byte(fmt.Sprintf("%s/%s", tokensKeyPrefix, signature))
return bs.db.Delete(key)
}
func (bs *BitcaskStore) LenTokens() int64 {
var count int64
if err := bs.db.Scan([]byte(tokensKeyPrefix), func(_ []byte) error {
count++
return nil
}); err != nil {
log.WithError(err).Error("error scanning")
}
return count
}

62
internal/cache.go

@ -21,7 +21,7 @@ import (
const (
feedCacheFile = "cache"
feedCacheVersion = 11 // increase this if breaking changes occur to cache file.
feedCacheVersion = 13 // increase this if breaking changes occur to cache file.
localViewKey = "local"
discoverViewKey = "discover"
@ -43,7 +43,7 @@ func FilterOutFeedsAndBotsFactory(conf *Config) FilterFunc {
if strings.HasPrefix(twter.URL, "https://search.twtxt.net") {
return false
}
if isLocal(twter.URL) && HasString(twtxtBots, twter.Nick) {
if isLocal(twter.URL) && HasString(automatedFeeds, twter.Nick) {
return false
}
return true
@ -101,19 +101,6 @@ func GroupTwtsBy(twts types.Twts, g GroupFunc) (res map[string]types.Twts) {
return
}
func MergeTwts(old, new types.Twts, max int) types.Twts {
twts := UniqTwts(append(old, new...))
sort.Sort(twts)
var offset int
if len(twts) > max {
offset = len(twts) - max
}
return twts[offset:]
}
func UniqTwts(twts types.Twts) (res types.Twts) {
seenTwts := make(map[string]bool)
for _, twt := range twts {
@ -141,21 +128,16 @@ func NewCached(twts types.Twts, lastModified string) *Cached {
}
// Update ...
func (cached *Cached) Update(url, lastmodiied string, twts types.Twts, maxSize int) {
cached.mu.Lock()
defer cached.mu.Unlock()
func (cached *Cached) Update(url, lastmodiied string, twts types.Twts) {
// Avoid overwriting a cached Feed with no Twts
if len(twts) == 0 {
return
}
if len(twts) >= maxSize {
cached.Twts = twts[:]
cached.LastModified = lastmodiied
return
}
cached.mu.Lock()
defer cached.mu.Unlock()
cached.Twts = MergeTwts(cached.Twts, twts, maxSize)
cached.Twts = twts
cached.LastModified = lastmodiied
}
@ -280,12 +262,16 @@ func (cache *Cache) FetchTwts(conf *Config, archive Archiver, feeds types.Feeds,
seenFeeds := make(map[string]bool)
for feed := range feeds {
// Normalize URLs
feed.URL = NormalizeURL(feed.URL)
// Skip feeds we've already fetched by URI
// (but possibly referenced by different alias)
// Also skil feeds that are blacklisted.
if _, seenFeed := seenFeeds[feed.URL]; seenFeed {
continue
}
// Skip feeds that are blacklisted.
if cache.conf.BlacklistedFeed(feed.URL) {
log.Warnf("attempt to fetch blacklisted feed %s", feed)
continue
@ -338,20 +324,13 @@ func (cache *Cache) FetchTwts(conf *Config, archive Archiver, feeds types.Feeds,
}
future, twts, old := types.SplitTwts(tf.Twts(), conf.MaxCacheTTL, conf.MaxCacheItems)
if len(future) > 0 {
log.Warnf(
"feed %s has %d posts in the future, possible bad client or misconfigured timezone",
feed, len(future),
)
log.Warnf("feed %s has %d posts in the future, possible bad client or misconfigured timezone", feed, len(future))
}
// If N == 0 we possibly exceeded conf.MaxFetchLimit when
// reading this feed. Log it and bump a cache_limited counter
if limitedReader.N <= 0 {
log.Warnf(
"feed size possibly exceeds MaxFetchLimit of %s for %s",
humanize.Bytes(uint64(conf.MaxFetchLimit)),
feed,
)
log.Warnf("feed size possibly exceeds MaxFetchLimit of %s for %s", humanize.Bytes(uint64(conf.MaxFetchLimit)), feed)
metrics.Counter("cache", "limited").Inc()
}
@ -467,20 +446,13 @@ func (cache *Cache) FetchTwts(conf *Config, archive Archiver, feeds types.Feeds,
}
future, twts, old := types.SplitTwts(tf.Twts(), conf.MaxCacheTTL, conf.MaxCacheItems)
if len(future) > 0 {
log.Warnf(
"feed %s has %d posts in the future, possible bad client or misconfigured timezone",
feed, len(future),
)
log.Warnf("feed %s has %d posts in the future, possible bad client or misconfigured timezone", feed, len(future))
}
// If N == 0 we possibly exceeded conf.MaxFetchLimit when
// reading this feed. Log it and bump a cache_limited counter
if limitedReader.N <= 0 {
log.Warnf(
"feed size possibly exceeds MaxFetchLimit of %s for %s",
humanize.Bytes(uint64(conf.MaxFetchLimit)),
feed,
)
log.Warnf("feed size possibly exceeds MaxFetchLimit of %s for %s", humanize.Bytes(uint64(conf.MaxFetchLimit)), feed)
metrics.Counter("cache", "limited").Inc()
}
@ -622,7 +594,7 @@ func (cache *Cache) UpdateFeed(url, lastmodified string, twts types.Twts) {
cache.Feeds[url] = NewCached(twts, lastmodified)
cache.mu.Unlock()
} else {
cached.Update(url, lastmodified, twts, cache.conf.MaxCacheItems)
cached.Update(url, lastmodified, twts)
}
}

24
internal/config.go

@ -51,6 +51,11 @@ type Settings struct {
WhitelistedImages []string `yaml:"whitelisted_images"`
BlacklistedFeeds []string `yaml:"blacklisted_feeds"`
Features *FeatureFlags `yaml:"features"`
// Pod Level Settings (overridable by Users)
DisplayDatesInTimezone string `yaml:"display_dates_in_timezone"`
DisplayTimePreference string `yaml:"display_time_preference"`
OpenLinksInPreference string `yaml:"open_links_in_preference"`
}
// SoftwareConfig contains the server version information
@ -126,6 +131,11 @@ type Config struct {
BlacklistedFeeds []string
Features *FeatureFlags
// Pod Level Settings (overridable by Users)
DisplayDatesInTimezone string
DisplayTimePreference string
OpenLinksInPreference string
}
var _ types.FmtOpts = (*Config)(nil)
@ -210,6 +220,13 @@ func (c *Config) Validate() error {
return fmt.Errorf("error applying blacklisted feeds: %w", err)
}
// Automatically correct missing Scheme in Pod Base URL
if c.baseURL.Scheme == "" {
log.Warnf("pod base url (-u/--base-url) %s is missing the scheme")
c.baseURL.Scheme = "http"
c.BaseURL = c.baseURL.String()
}
if c.Debug {
return nil
}
@ -230,6 +247,13 @@ func (c *Config) Validate() error {
return fmt.Errorf("error: api signing key is not configured")
}
// Automatically correct missing Scheme in Pod Base URL
if c.baseURL.Scheme == "" {
log.Warnf("pod base url (-u/--base-url) %s is missing the scheme")
c.baseURL.Scheme = "https"
c.BaseURL = c.baseURL.String()
}
return nil
}

20
internal/context.go

@ -74,6 +74,10 @@ type Context struct {
Authenticated bool
IsAdmin bool
DisplayDatesInTimezone string
DisplayTimePreference string
OpenLinksInPreference string
Error bool
Message string
Lang string // language
@ -149,6 +153,10 @@ func NewContext(s *Server, req *http.Request) *Context {
BlacklistedFeeds: conf.BlacklistedFeeds,
EnabledFeatures: conf.Features.AsStrings(),
DisplayDatesInTimezone: conf.DisplayDatesInTimezone,
DisplayTimePreference: conf.DisplayTimePreference,
OpenLinksInPreference: conf.OpenLinksInPreference,
Commit: yarn.Commit,
Theme: conf.Theme,
Lang: conf.Lang,
@ -194,14 +202,12 @@ func NewContext(s *Server, req *http.Request) *Context {
}
ctx.User = user
tokens, err := db.GetUserTokens(user)
if err != nil {
log.WithError(err).Warnf("error loading tokens for %s", ctx.Username)
}
ctx.Tokens = tokens
} else {
ctx.User = &User{}
ctx.User = &User{
DisplayDatesInTimezone: conf.DisplayDatesInTimezone,
DisplayTimePreference: conf.DisplayTimePreference,
OpenLinksInPreference: conf.OpenLinksInPreference,
}
ctx.Twter = types.Twter{}
}

1
internal/conversation_handler.go

@ -82,7 +82,6 @@ func (s *Server) ConversationHandler() httprouter.Handle {
ks = append(ks, tags.Tags()...)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Last-Modified", twt.Created().Format(http.TimeFormat))
if strings.HasPrefix(twt.Twter().URL, s.config.BaseURL) {
w.Header().Set(
"Link",

119
internal/email.go

@ -14,6 +14,21 @@ import (
var (
ErrSendingEmail = errors.New("error: unable to send email")
magicLinkAuthEmailTemplate = template.Must(template.New("email").Parse(`Hello {{ .Username }},
You have requested to login to your Yarn.social account on on {{ .Pod }} via email.
**IMPORTANT:** If this was __NOT__ initiated by you, please ignore this email and contact support!
To login to your account, please visit the following link:
{{ .BaseURL}}/magiclinkauth?token={{ .Token }}
Kind regards,
{{ .Pod}} Support
`))
passwordResetEmailTemplate = template.Must(template.New("email").Parse(`Hello {{ .Username }},
You have requested to have your password on {{ .Pod }} reset for your account.
@ -42,6 +57,27 @@ Kind regards,
{{ .Pod}} Support
`))
candidatesForDeletionEmailTemplate = template.Must(template.New("email").Parse(`Hello {{ .AdminUser }},
The following top 10 users are candidates for deletion as they have either never posted, updated their profile
or follow any feeds. Their usernames on {{ .Pod }} are shown below with their scores. The higher the score
the more likely the user has never used their account.
{{ range $candidate := .Candidates }}
{{ $candidate.Username }}
Score: {{ $candidate.Score }}
{{ $.BaseURL }}/user/{{ $candidate.Username }}
{{ end }}
To delete any of these users visit:
{{ .BaseURL}}/manage/users
Kind regards,
{{ .Pod }} Support
`))
reportAbuseEmailTemplate = template.Must(template.New("email").Parse(`Hello {{ .AdminUser }},
{{ .Name }} <{{ .Email }} from {{ .Pod }} has sent the following abuse report:
@ -61,6 +97,14 @@ Kind regards,
`))
)
type MagicLinkAuthContext struct {
Pod string
BaseURL string
Token string
Username string
}
type PasswordResetEmailContext struct {
Pod string
BaseURL string
@ -79,6 +123,25 @@ type SupportRequestEmailContext struct {
Message string
}
type DeletionCandidate struct {
Username string
Score int
}
type CandidatesByScore []DeletionCandidate
func (c CandidatesByScore) Len() int { return len(c) }
func (c CandidatesByScore) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c CandidatesByScore) Less(i, j int) bool { return c[i].Score < c[j].Score }
type CandidatesForDeletionEmailContext struct {
Pod string
BaseURL string
AdminUser string
Candidates []DeletionCandidate
}
type ReportAbuseEmailContext struct {
Pod string
AdminUser string
@ -127,6 +190,34 @@ func SendEmail(conf *Config, recipients []string, replyTo, subject string, body
return nil
}
func SendMagicLinkAuthEmail(conf *Config, user *User, email, token string) error {
recipients := []string{email}
subject := fmt.Sprintf(
"[%s]: Login via Email for %s",
conf.Name, user.Username,
)
ctx := MagicLinkAuthContext{
Pod: conf.Name,
BaseURL: conf.BaseURL,
Token: token,
Username: user.Username,
}
buf := &bytes.Buffer{}
if err := magicLinkAuthEmailTemplate.Execute(buf, ctx); err != nil {
log.WithError(err).Error("error rendering email template")
return err
}
if err := SendEmail(conf, recipients, conf.SMTPFrom, subject, buf.String()); err != nil {
log.WithError(err).Errorf("error sending new token to %s", recipients[0])
return err
}
return nil
}
func SendPasswordResetEmail(conf *Config, user *User, email, token string) error {
recipients := []string{email}
subject := fmt.Sprintf(
@ -185,6 +276,34 @@ func SendSupportRequestEmail(conf *Config, name, email, subject, message string)
return nil
}
func SendCandidatesForDeletionEmail(conf *Config, candidates []DeletionCandidate) error {
recipients := []string{conf.AdminEmail}
emailSubject := fmt.Sprintf(
"[%s Candidates for Deletion]: %d users",
conf.Name, len(candidates),
)
ctx := CandidatesForDeletionEmailContext{
Pod: conf.Name,
BaseURL: conf.BaseURL,
AdminUser: conf.AdminUser,
Candidates: candidates,
}
buf := &bytes.Buffer{}
if err := candidatesForDeletionEmailTemplate.Execute(buf, ctx); err != nil {
log.WithError(err).Error("error rendering email template")
return err
}
if err := SendEmail(conf, recipients, conf.SMTPFrom, emailSubject, buf.String()); err != nil {
log.WithError(err).Errorf("error sending candidates for deletoin email to %s", recipients[0])
return err
}
return nil
}
func SendReportAbuseEmail(conf *Config, nick, url, name, email, category, message string) error {
recipients := []string{conf.AdminEmail, email}
emailSubject := fmt.Sprintf(

10
internal/external_handlers.go

@ -34,10 +34,6 @@ func (s *Server) ExternalHandler() httprouter.Handle {
return
}
if nick == "" {
log.Warn("no nick given to external profile request")
}
if !s.cache.IsCached(uri) {
s.tasks.DispatchFunc(func() error {
sources := make(types.Feeds)
@ -161,10 +157,6 @@ func (s *Server) ExternalFollowingHandler() httprouter.Handle {
return
}
if nick == "" {
log.Warn("no nick given to external profile request")
}
if !s.cache.IsCached(uri) {
sources := make(types.Feeds)
sources[types.Feed{Nick: nick, URL: uri}] = true
@ -272,7 +264,6 @@ func (s *Server) ExternalAvatarHandler() httprouter.Handle {
uri := r.URL.Query().Get("uri")
if uri == "" {
log.Warn("no uri provided for external avatar")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
@ -282,7 +273,6 @@ func (s *Server) ExternalAvatarHandler() httprouter.Handle {
w.Header().Set("Content-Type", "image/png")
if !FileExists(fn) {
log.Warnf("no external avatar found for %s", slug)
http.Error(w, "External avatar not found", http.StatusNotFound)
return
}

35
internal/features.go

@ -15,7 +15,7 @@ const (
// FeatureInvalid is the invalid feature (0)
FeatureInvalid FeatureType = iota
FeatureFoo
FeatureBar
FeatureMagicLinkAuth
)
// Interface guards
@ -30,8 +30,8 @@ func (f FeatureType) String() string {
switch f {
case FeatureFoo:
return "foo"
case FeatureBar:
return "bar"
case FeatureMagicLinkAuth:
return "magic_link_auth"
default:
return "invalid_feature"
}
@ -49,13 +49,21 @@ func AvailableFeatures() []string {
return features
}
func IsFeatureEnabled(fs *FeatureFlags, name string) bool {
feature, err := FeatureFromString(name)
if err != nil {
return false
}
return fs.IsEnabled(feature)
}
func FeatureFromString(s string) (FeatureType, error) {
s = strings.TrimSpace(strings.ToLower(s))
switch s {
case "foo":
return FeatureFoo, nil
case "bar":
return FeatureBar, nil
case "magic_link_auth":
return FeatureMagicLinkAuth, nil
default:
fs := fmt.Sprintf("available features: %s", strings.Join(AvailableFeatures(), " "))
return FeatureInvalid, fmt.Errorf("Error unrecognised feature: %s (%s)", s, fs)
@ -146,26 +154,33 @@ func (f *FeatureFlags) IsEnabled(feature FeatureType) bool {
}
func (f *FeatureFlags) MarshalJSON() ([]byte, error) {
var vs []FeatureType
var vs []string
f.RLock()
for flag := range f.flags {
vs = append(vs, flag)
vs = append(vs, flag.String())
}
f.RUnlock()
return json.Marshal(vs)
}
func (f *FeatureFlags) UnmarshalJSON(b []byte) error {
var vs []FeatureType
var vs []string
if err := json.Unmarshal(b, &vs); err != nil {
return err
}
features, err := FeaturesFromStrings(vs)
if err != nil {
return err
}
f.Lock()
f.flags = make(map[FeatureType]bool)
for _, v := range vs {
f.flags[v] = true
for _, feature := range features {
f.flags[feature] = true
}
f.Unlock()
return nil
}

372
internal/feeds_handlers.go

@ -0,0 +1,372 @@
package internal
import (
"fmt"
"net/http"
"path/filepath"
"github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
)
// FeedHandler ...
func (s *Server) FeedHandler() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
ctx := NewContext(s, r)
name := NormalizeFeedName(r.FormValue("name"))
trdata := map[string]interface{}{}
if err := ValidateFeedName(s.config.Data, name); err != nil {
ctx.Error = true
trdata["Error"] = err.Error()
ctx.Message = s.tr(ctx, "ErrorInvalidFeedName", trdata)
s.render("error", w, ctx)
return
}
if err := CreateFeed(s.config, s.db, ctx.User, name, false); err != nil {
ctx.Error = true
trdata["Error"] = err.Error()
ctx.Message = s.tr(ctx, "ErrorCreateFeed", trdata)
s.render("error", w, ctx)
return
}
ctx.User.Follow(name, URLForUser(s.config.BaseURL, name))
if err := s.db.SetUser(ctx.Username, ctx.User); err != nil {
ctx.Error = true
trdata["Error"] = err.Error()
ctx.Message = s.tr(ctx, "ErrorCreateFeed", trdata)
s.render("error", w, ctx)
return
}
if _, err := AppendSpecial(
s.config, s.db,
twtxtBot,
fmt.Sprintf(
"FEED: @<%s %s> from @<%s %s>",
name, URLForUser(s.config.BaseURL, name),
ctx.User.Username, URLForUser(s.config.BaseURL, ctx.User.Username),
),
); err != nil {
log.WithError(err).Warnf("error appending special FOLLOW post")
}
ctx.Error = false
trdata["Feed"] = name
ctx.Message = s.tr(ctx, "MsgCreateFeedSuccess", trdata)
s.render("error", w, ctx)
}
}
// FeedsHandler ...
func (s *Server) FeedsHandler() httprouter.Handle {
isAdminUser := IsAdminUserFactory(s.config)
canManageFeed := func(feed string, u *User) bool {
if u.OwnsFeed(feed) {
return true
}
if IsSpecialFeed(feed) && isAdminUser(u) {
return true
}
return false
}
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
ctx := NewContext(s, r)
allFeeds, err := s.db.GetAllFeeds()
if err != nil {
ctx.Error = true
ctx.Message = s.tr(ctx, "ErrorLoadingFeeds")
s.render("error", w, ctx)
return
}
feedSources, err := LoadFeedSources(s.config.Data)
if err != nil {
ctx.Error = true
ctx.Message = s.tr(ctx, "ErrorLoadingFeeds")
s.render("error", w, ctx)
return
}
var (
userFeeds []*Feed
localFeeds []*Feed