Merge branch 'main' into profile-boosts
120
README.md
|
@ -11,7 +11,7 @@ With GoToSocial, you can keep in touch with your friends, post, read, and share
|
||||||
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/sloth.webp" width="300"/>
|
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/sloth.webp" width="300"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**GoToSocial is still [ALPHA SOFTWARE](https://en.wikipedia.org/wiki/Software_release_life_cycle#Alpha)**. It is already deployable and useable, and it federates cleanly with many other Fediverse servers (not yet all). However, many things are not yet implemented, and there are plenty of bugs! We foresee entering beta around the beginning of 2024.
|
**GoToSocial is still [BETA SOFTWARE](https://en.wikipedia.org/wiki/Software_release_life_cycle#Beta)**. It is already deployable and useable, and it federates cleanly with many other Fediverse servers (not yet all). However, many things are not yet implemented, and there are plenty of bugs! We left alpha stage around September/October 2024, and we intend to exit beta some time around 2026.
|
||||||
|
|
||||||
Documentation is at [docs.gotosocial.org](https://docs.gotosocial.org). You can skip straight to the API documentation [here](https://docs.gotosocial.org/en/latest/api/swagger/).
|
Documentation is at [docs.gotosocial.org](https://docs.gotosocial.org). You can skip straight to the API documentation [here](https://docs.gotosocial.org/en/latest/api/swagger/).
|
||||||
|
|
||||||
|
@ -29,8 +29,12 @@ Here's a screenshot of the instance landing page!
|
||||||
- [History and Status](#history-and-status)
|
- [History and Status](#history-and-status)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Mastodon API compatibility](#mastodon-api-compatibility)
|
- [Mastodon API compatibility](#mastodon-api-compatibility)
|
||||||
- [Granular post settings](#granular-post-settings)
|
- [Granular post visibility settings](#granular-post-visibility-settings)
|
||||||
- [Customizability](#customizability)
|
- [Reply controls](#reply-controls)
|
||||||
|
- [Local-only posting](#local-only-posting)
|
||||||
|
- [RSS feed](#rss-feed)
|
||||||
|
- [Rich text formatting](#rich-text-formatting)
|
||||||
|
- [Themes and custom CSS](#themes-and-custom-css)
|
||||||
- [Easy to run](#easy-to-run)
|
- [Easy to run](#easy-to-run)
|
||||||
- [Safety + security features](#safety--security-features)
|
- [Safety + security features](#safety--security-features)
|
||||||
- [Various federation modes](#various-federation-modes)
|
- [Various federation modes](#various-federation-modes)
|
||||||
|
@ -90,7 +94,9 @@ This project sprang up in February/March 2021 out of a dissatisfaction with the
|
||||||
|
|
||||||
It began as a solo project, and then picked up steam as more developers became interested and jumped on.
|
It began as a solo project, and then picked up steam as more developers became interested and jumped on.
|
||||||
|
|
||||||
For a detailed view on what's implemented and what's not, and progress made towards [beta release](https://en.wikipedia.org/wiki/Software_release_life_cycle#Beta), please see [the roadmap document](https://github.com/superseriousbusiness/gotosocial/blob/main/ROADMAP.md).
|
We made our first Alpha release in November 2021. We left Alpha and entered Beta in September/October 2024.
|
||||||
|
|
||||||
|
For a detailed view on what's implemented and what's not, and progress made towards [stable release](https://en.wikipedia.org/wiki/Software_release_life_cycle#Stable_release), please see [the roadmap document](https://github.com/superseriousbusiness/gotosocial/blob/main/ROADMAP.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -100,44 +106,120 @@ For a detailed view on what's implemented and what's not, and progress made towa
|
||||||
|
|
||||||
The Mastodon API has become the de facto standard for client communication with federated servers, so GoToSocial has implemented and extended the API with custom functionality.
|
The Mastodon API has become the de facto standard for client communication with federated servers, so GoToSocial has implemented and extended the API with custom functionality.
|
||||||
|
|
||||||
Though most apps that implement the Mastodon API should work, GoToSocial works reliably with beautiful apps like:
|
Though most apps that implement the Mastodon API should work, GoToSocial is tested and works reliably with beautiful apps like:
|
||||||
|
|
||||||
* [Tusky](https://tusky.app/) for Android
|
* [Tusky](https://tusky.app/) for Android
|
||||||
* [Semaphore](https://semaphore.social/) in the browser
|
* [Semaphore](https://semaphore.social/) in the browser
|
||||||
* [Feditext](https://github.com/feditext/feditext) (beta) on iOS, iPadOS and macOS
|
* [Feditext](https://github.com/feditext/feditext) (beta) on iOS, iPadOS and macOS
|
||||||
|
|
||||||
If you've used Mastodon with any of these apps before, you'll find using GoToSocial a breeze.
|
If you've used Mastodon with a third-party app before, you'll find using GoToSocial a breeze.
|
||||||
|
|
||||||
### Granular post settings
|
### Granular post visibility settings
|
||||||
|
|
||||||
It's important that when you post something, you can choose who sees it.
|
It's important that when you post something, you can choose who sees it.
|
||||||
|
|
||||||
GoToSocial offers public/unlisted/friends-only/mutuals-only/and direct posts (slide in DMs! -- with consent).
|
GoToSocial offers public, unlisted/unlocked, followers-only, and direct posts (slide in DMs! -- with consent).
|
||||||
|
|
||||||
It also allows you to customize how people interact with your posts:
|
### Reply controls
|
||||||
|
|
||||||
- Local-only posts.
|
GoToSocial lets you choose who can reply to your posts, via [interaction policies](https://docs.gotosocial.org/en/latest/user_guide/settings/#default-interaction-policies). You can choose to let anyone reply to your posts, let only your friends reply, and more.
|
||||||
- Rebloggable/boostable toggle.
|
|
||||||
- 'Likeable' toggle.
|
|
||||||
- 'Replyable' toggle.
|
|
||||||
|
|
||||||
### Customizability
|
![interaction policies settings](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/user-settings-interaction-policy-1.png)
|
||||||
|
|
||||||
|
### Local-only posting
|
||||||
|
|
||||||
|
Sometimes you only want to talk to people you share an instance with. GoToSocial supports this via local-only posting, which ensures that your post stays on your instance only. (Local-only posting is currently dependent on client support.)
|
||||||
|
|
||||||
|
### RSS feed
|
||||||
|
|
||||||
|
GoToSocial lets you opt-in to exposing your profile as an RSS feed, so that people can subscribe to your public feed without missing a post.
|
||||||
|
|
||||||
|
### Rich text formatting
|
||||||
|
|
||||||
|
With GoToSocial, you can write posts using the popular, easy-to-use Markdown markup language, which lets you produce rich HTML posts with support for blockquotes, syntax-highlighted code blocks, lists, inline links, and more.
|
||||||
|
|
||||||
|
![markdown-formatted post](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/markdown-post.png)
|
||||||
|
|
||||||
|
### Themes and custom CSS
|
||||||
|
|
||||||
Users can [choose from a variety of fun themes](https://docs.gotosocial.org/en/latest/user_guide/settings/#select-theme) for their profile, or even write their own [custom CSS](https://docs.gotosocial.org/en/latest/user_guide/settings/#custom-css).
|
Users can [choose from a variety of fun themes](https://docs.gotosocial.org/en/latest/user_guide/settings/#select-theme) for their profile, or even write their own [custom CSS](https://docs.gotosocial.org/en/latest/user_guide/settings/#custom-css).
|
||||||
|
|
||||||
Plenty of [config options](https://github.com/superseriousbusiness/gotosocial/blob/main/example/config.yaml) for admins to play around with too.
|
It's also easy for admins to [add their own custom themes](https://docs.gotosocial.org/en/latest/admin/themes/) for users to choose from.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show theme examples</summary>
|
||||||
|
<figure>
|
||||||
|
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/theme-blurple-dark.png"/>
|
||||||
|
<figcaption>Blurple dark</figcaption>
|
||||||
|
</figure>
|
||||||
|
<hr/>
|
||||||
|
<figure>
|
||||||
|
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/theme-blurple-light.png"/>
|
||||||
|
<figcaption>Blurple light</figcaption>
|
||||||
|
</figure>
|
||||||
|
<hr/>
|
||||||
|
<figure>
|
||||||
|
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/theme-brutalist-light.png"/>
|
||||||
|
<figcaption>Brutalist light</figcaption>
|
||||||
|
</figure>
|
||||||
|
<hr/>
|
||||||
|
<figure>
|
||||||
|
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/theme-brutalist-dark.png"/>
|
||||||
|
<figcaption>Brutalist dark</figcaption>
|
||||||
|
</figure>
|
||||||
|
<hr/>
|
||||||
|
<figure>
|
||||||
|
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/theme-ecks-pee.png"/>
|
||||||
|
<figcaption>Ecks pee</figcaption>
|
||||||
|
</figure>
|
||||||
|
<hr/>
|
||||||
|
<figure>
|
||||||
|
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/theme-midnight-trip.png"/>
|
||||||
|
<figcaption>Midnight trip</figcaption>
|
||||||
|
</figure>
|
||||||
|
<hr/>
|
||||||
|
<figure>
|
||||||
|
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/theme-rainforest.png"/>
|
||||||
|
<figcaption>Rainforest</figcaption>
|
||||||
|
</figure>
|
||||||
|
<hr/>
|
||||||
|
<figure>
|
||||||
|
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/theme-soft.png"/>
|
||||||
|
<figcaption>Soft</figcaption>
|
||||||
|
</figure>
|
||||||
|
<hr/>
|
||||||
|
<figure>
|
||||||
|
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/theme-solarized-dark.png"/>
|
||||||
|
<figcaption>Solarized dark</figcaption>
|
||||||
|
</figure>
|
||||||
|
<hr/>
|
||||||
|
<figure>
|
||||||
|
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/theme-solarized-light.png"/>
|
||||||
|
<figcaption>Solarized light</figcaption>
|
||||||
|
</figure>
|
||||||
|
<hr/>
|
||||||
|
<figure>
|
||||||
|
<img src="https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/theme-sunset.png"/>
|
||||||
|
<figcaption>Sunset</figcaption>
|
||||||
|
</figure>
|
||||||
|
<hr/>
|
||||||
|
</details>
|
||||||
|
|
||||||
### Easy to run
|
### Easy to run
|
||||||
|
|
||||||
No external dependencies apart from a database (or just use SQLite!). Simply download the binary + assets (or Docker container), and run.
|
GoToSocial uses only about 250-350MiB of RAM, and requires very little CPU power, so it plays nice with single-board computers, old laptops and tiny $5/month VPSes.
|
||||||
|
|
||||||
GoToSocial uses only about 150-250MiB of RAM, so it plays nice with single-board computers, old laptops and tiny $5/month VPSes.
|
![Grafana graph showing GoToSocial heap in use hovering around 250MB and spiking occasionally to 400MB-500MB.](https://raw.githubusercontent.com/superseriousbusiness/gotosocial/main/docs/assets/getting-started-memory-graph.png)
|
||||||
|
|
||||||
|
No external dependencies apart from a database (or just use SQLite!).
|
||||||
|
|
||||||
|
Simply download the binary + assets (or Docker container), tweak your configuration, and run.
|
||||||
|
|
||||||
### Safety + security features
|
### Safety + security features
|
||||||
|
|
||||||
- Built-in, automatic support for secure HTTPS with [Let's Encrypt](https://letsencrypt.org/).
|
- Built-in, automatic support for secure HTTPS with [Let's Encrypt](https://letsencrypt.org/).
|
||||||
- Strict privacy enforcement for posts and strict blocking logic.
|
- Strict privacy enforcement for posts and strict blocking logic.
|
||||||
- Import and export allow lists and deny lists. Subscribe to community-created block lists (think Ad blocker, but for federation!).
|
- Import and export allow lists and deny lists. Subscribe to community-created block lists (think Ad blocker, but for federation!) (feature still in progress).
|
||||||
- HTTP signature authentication: GoToSocial requires [HTTP Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12) when sending and receiving messages, to ensure that your messages can't be tampered with and your identity can't be forged.
|
- HTTP signature authentication: GoToSocial requires [HTTP Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12) when sending and receiving messages, to ensure that your messages can't be tampered with and your identity can't be forged.
|
||||||
|
|
||||||
### Various federation modes
|
### Various federation modes
|
||||||
|
@ -166,7 +248,7 @@ On top of this API, web developers are encouraged to build any front-end impleme
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
Since GoToSocial is still in alpha, there are plenty of bugs. We use [GitHub issues](https://github.com/superseriousbusiness/gotosocial/issues?q=is%3Aissue+is%3Aopen+label%3Abug) to track these.
|
Since GoToSocial is still in beta, there are plenty of bugs. We use [GitHub issues](https://github.com/superseriousbusiness/gotosocial/issues?q=is%3Aissue+is%3Aopen+label%3Abug) to track these.
|
||||||
|
|
||||||
Since every ActivityPub server implementation has a slightly different interpretation of the protocol, some servers don't quite federate properly with GoToSocial yet. We're tracking these issues [in this project](https://github.com/superseriousbusiness/gotosocial/projects/4). Eventually, we want to make sure that any implementation that can federate nicely with Mastodon should also be able to federate with GoToSocial.
|
Since every ActivityPub server implementation has a slightly different interpretation of the protocol, some servers don't quite federate properly with GoToSocial yet. We're tracking these issues [in this project](https://github.com/superseriousbusiness/gotosocial/projects/4). Eventually, we want to make sure that any implementation that can federate nicely with Mastodon should also be able to federate with GoToSocial.
|
||||||
|
|
||||||
|
|
15
ROADMAP.md
|
@ -1,10 +1,10 @@
|
||||||
# Roadmap to Beta <!-- omit in toc -->
|
# Roadmap to Beta <!-- omit in toc -->
|
||||||
|
|
||||||
This document contains the roadmap for GoToSocial to be considered eligible for its first [beta release](https://en.wikipedia.org/wiki/Software_release_life_cycle#Beta).
|
This document contains the roadmap for GoToSocial to be considered eligible for its first proper [stable release](https://en.wikipedia.org/wiki/Software_release_life_cycle#Stable_release).
|
||||||
|
|
||||||
All the info contained in this document is best-guess only. It's useful to have a rough timeline we can direct people to, but things will undoubtedly change along the way; don't hold us to anything in this doc!
|
All the info contained in this document is best-guess only. It's useful to have a rough timeline we can direct people to, but things will undoubtedly change along the way; don't hold us to anything in this doc!
|
||||||
|
|
||||||
Thank you to [NLnet](https://nlnet.nl) for helping to fund the alpha phase of GoToSocial development and get us moving towards beta!
|
Thank you to [NLnet](https://nlnet.nl) for helping to fund the alpha and beta phases of GoToSocial development!
|
||||||
|
|
||||||
Big thank you to all of our [Open Collective](https://opencollective.com/gotosocial) and [Liberapay](https://liberapay.com/gotosocial) contributors, who've helped us keep the lights on! 💕
|
Big thank you to all of our [Open Collective](https://opencollective.com/gotosocial) and [Liberapay](https://liberapay.com/gotosocial) contributors, who've helped us keep the lights on! 💕
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ Big thank you to all of our [Open Collective](https://opencollective.com/gotosoc
|
||||||
- [Early 2024](#early-2024)
|
- [Early 2024](#early-2024)
|
||||||
- [BETA milestone](#beta-milestone)
|
- [BETA milestone](#beta-milestone)
|
||||||
- [Remainder 2024 - early 2025](#remainder-2024---early-2025)
|
- [Remainder 2024 - early 2025](#remainder-2024---early-2025)
|
||||||
|
- [On the way out of BETA to STABLE RELEASE](#on-the-way-out-of-beta-to-stable-release)
|
||||||
- [Wishlist](#wishlist)
|
- [Wishlist](#wishlist)
|
||||||
|
|
||||||
## Beta Aims
|
## Beta Aims
|
||||||
|
@ -61,7 +62,7 @@ What follows is a rough timeline of features that will be implemented on the roa
|
||||||
|
|
||||||
### BETA milestone
|
### BETA milestone
|
||||||
|
|
||||||
Completion of all above features indicates that we are now in the BETA phase of GoToSocial. We foresee this happening around Feb/March 2024.
|
Completion of all above features indicates that we are now in the BETA phase of GoToSocial. We foresee this happening around Feb/March 2024. EDIT: It ended up happening in September/October 2024, whoops!
|
||||||
|
|
||||||
### Remainder 2024 - early 2025
|
### Remainder 2024 - early 2025
|
||||||
|
|
||||||
|
@ -69,9 +70,9 @@ These are provided in no specific order.
|
||||||
|
|
||||||
- [x] **Filters v2** -- implement v2 of the filters API.
|
- [x] **Filters v2** -- implement v2 of the filters API.
|
||||||
- [x] **Mute accounts** -- mute accounts to prevent their posts showing up in your home timeline (optional: for limited period of time).
|
- [x] **Mute accounts** -- mute accounts to prevent their posts showing up in your home timeline (optional: for limited period of time).
|
||||||
- [ ] **Non-replyable posts** -- design a non-replyable post path for GoToSocial based on https://github.com/mastodon/mastodon/issues/14762#issuecomment-1196889788; allow users to create non-replyable posts.
|
- [x] **Non-replyable posts** -- design a non-replyable post path for GoToSocial based on https://github.com/mastodon/mastodon/issues/14762#issuecomment-1196889788; allow users to create non-replyable posts.
|
||||||
- [ ] **Block + allow list subscriptions** -- allow instance admins to subscribe their instance to plaintext domain block/allow lists (much of the work for this is already in place).
|
- [ ] **Block + allow list subscriptions** -- allow instance admins to subscribe their instance to plaintext domain block/allow lists (much of the work for this is already in place).
|
||||||
- [ ] **Direct conversation view** -- allow users to easily page through all direct-message conversations they're a part of.
|
- [x] **Direct conversation view** -- allow users to easily page through all direct-message conversations they're a part of.
|
||||||
- [ ] **Oauth token management** -- create / view / invalidate OAuth tokens via the settings panel.
|
- [ ] **Oauth token management** -- create / view / invalidate OAuth tokens via the settings panel.
|
||||||
- [ ] **Status EDIT support** -- edit statuses that you've created, without having to delete + redraft. Federate edits out properly.
|
- [ ] **Status EDIT support** -- edit statuses that you've created, without having to delete + redraft. Federate edits out properly.
|
||||||
- [ ] **Fediverse relay support** -- publish posts to relays, pull posts from relays.
|
- [ ] **Fediverse relay support** -- publish posts to relays, pull posts from relays.
|
||||||
|
@ -80,6 +81,10 @@ These are provided in no specific order.
|
||||||
|
|
||||||
More tbd!
|
More tbd!
|
||||||
|
|
||||||
|
### On the way out of BETA to STABLE RELEASE
|
||||||
|
|
||||||
|
Tbd.
|
||||||
|
|
||||||
## Wishlist
|
## Wishlist
|
||||||
|
|
||||||
These cool things will be implemented if time allows (because we really want them):
|
These cool things will be implemented if time allows (because we really want them):
|
||||||
|
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 650 KiB |
After Width: | Height: | Size: 652 KiB |
After Width: | Height: | Size: 635 KiB |
After Width: | Height: | Size: 639 KiB |
After Width: | Height: | Size: 855 KiB |
After Width: | Height: | Size: 631 KiB |
After Width: | Height: | Size: 637 KiB |
After Width: | Height: | Size: 680 KiB |
After Width: | Height: | Size: 658 KiB |
After Width: | Height: | Size: 663 KiB |
After Width: | Height: | Size: 653 KiB |
|
@ -36,6 +36,6 @@ To see posts, you have to start following people! Once you've followed a few peo
|
||||||
|
|
||||||
We introduced a sign-up flow in v0.16.0. The server you want to sign up to must have enabled registrations/sign-ups, as detailed [right here](./admin/signups.md).
|
We introduced a sign-up flow in v0.16.0. The server you want to sign up to must have enabled registrations/sign-ups, as detailed [right here](./admin/signups.md).
|
||||||
|
|
||||||
## Why's it still in alpha?
|
## Why's it still in Beta?
|
||||||
|
|
||||||
Take a look at the [list of open bugs](https://github.com/superseriousbusiness/gotosocial/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and the [roadmap](https://github.com/superseriousbusiness/gotosocial/blob/main/ROADMAP.md) for a more detailed rundown.
|
Take a look at the [list of open bugs](https://github.com/superseriousbusiness/gotosocial/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and the [roadmap](https://github.com/superseriousbusiness/gotosocial/blob/main/ROADMAP.md) for a more detailed rundown.
|
||||||
|
|
|
@ -20,7 +20,7 @@ You can find more detail on system requirements below, but in short you should a
|
||||||
|
|
||||||
For a small instance (1-20 active users), GoToSocial will likely hover consistently between 250MB and 350MB of RAM usage once the internal caches are hydrated:
|
For a small instance (1-20 active users), GoToSocial will likely hover consistently between 250MB and 350MB of RAM usage once the internal caches are hydrated:
|
||||||
|
|
||||||
![Grafana graph showing GoToSocial heap in use hovering around 300MB and spiking occasionally to 400MB-500MB.](../assets/getting-started-memory-graph.png)
|
![Grafana graph showing GoToSocial heap in use hovering around 250MB and spiking occasionally to 400MB-500MB.](../assets/getting-started-memory-graph.png)
|
||||||
|
|
||||||
In the graph above you can see that RAM usage spikes during periods of load. This happens, for example, when when a status gets boosted by someone with many followers, or when the embedded `ffmpeg` binary is decoding or reencoding media files into thumbnails (especially larger video files).
|
In the graph above you can see that RAM usage spikes during periods of load. This happens, for example, when when a status gets boosted by someone with many followers, or when the embedded `ffmpeg` binary is decoding or reencoding media files into thumbnails (especially larger video files).
|
||||||
|
|
||||||
|
|
20
go.mod
|
@ -46,7 +46,7 @@ require (
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/ncruces/go-sqlite3 v0.18.3
|
github.com/ncruces/go-sqlite3 v0.18.3
|
||||||
github.com/oklog/ulid v1.3.1
|
github.com/oklog/ulid v1.3.1
|
||||||
github.com/prometheus/client_golang v1.20.3
|
github.com/prometheus/client_golang v1.20.4
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
|
@ -64,14 +64,14 @@ require (
|
||||||
github.com/uptrace/bun/extra/bunotel v1.2.1
|
github.com/uptrace/bun/extra/bunotel v1.2.1
|
||||||
github.com/wagslane/go-password-validator v0.3.0
|
github.com/wagslane/go-password-validator v0.3.0
|
||||||
github.com/yuin/goldmark v1.7.4
|
github.com/yuin/goldmark v1.7.4
|
||||||
go.opentelemetry.io/otel v1.30.0
|
go.opentelemetry.io/otel v1.29.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.52.0
|
go.opentelemetry.io/otel/exporters/prometheus v0.51.0
|
||||||
go.opentelemetry.io/otel/metric v1.30.0
|
go.opentelemetry.io/otel/metric v1.29.0
|
||||||
go.opentelemetry.io/otel/sdk v1.30.0
|
go.opentelemetry.io/otel/sdk v1.29.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.30.0
|
go.opentelemetry.io/otel/sdk/metric v1.29.0
|
||||||
go.opentelemetry.io/otel/trace v1.30.0
|
go.opentelemetry.io/otel/trace v1.29.0
|
||||||
go.uber.org/automaxprocs v1.5.3
|
go.uber.org/automaxprocs v1.5.3
|
||||||
golang.org/x/crypto v0.27.0
|
golang.org/x/crypto v0.27.0
|
||||||
golang.org/x/image v0.20.0
|
golang.org/x/image v0.20.0
|
||||||
|
@ -208,7 +208,7 @@ require (
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
|
40
go.sum
|
@ -468,8 +468,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4=
|
github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
|
||||||
github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
|
@ -638,24 +638,24 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
|
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||||
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
|
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 h1:m0yTiGDLUvVYaTFbAvCkVYIYcvwKt3G7OLoN77NUs/8=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0/go.mod h1:wBQbT4UekBfegL2nx0Xk1vBcnzyBPsIVm9hRG4fYcr4=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 h1:JAv0Jwtl01UFiyWZEMiJZBiTlv5A50zNs8lsthXqIio=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0/go.mod h1:QNKLmUEAq2QUbPQUfvw4fmv0bgbK7UlOSFCnXyfvSNc=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.52.0 h1:kmU3H0b9ufFSi8IQCcxack+sWUblKkFbqWYs6YiACGQ=
|
go.opentelemetry.io/otel/exporters/prometheus v0.51.0 h1:G7uexXb/K3T+T9fNLCCKncweEtNEBMTO+46hKX5EdKw=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.52.0/go.mod h1:+wsAp2+JhuGXX7YRkjlkx6hyWY3ogFPfNA4x3nyiAh0=
|
go.opentelemetry.io/otel/exporters/prometheus v0.51.0/go.mod h1:v0mFe5Kk7woIh938mrZBJBmENYquyA0IICrlYm4Y0t4=
|
||||||
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
|
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
|
||||||
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
|
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
||||||
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
|
go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
|
||||||
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
|
go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792jZO1bo4BXkM=
|
go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y=
|
go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ=
|
||||||
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
|
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||||
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
|
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||||
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
package statuses_test
|
package statuses_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
@ -25,6 +31,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
@ -59,6 +66,113 @@ type StatusStandardTestSuite struct {
|
||||||
statusModule *statuses.Module
|
statusModule *statuses.Module
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalizes a status response to a determinate
|
||||||
|
// form, and pretty-prints it to JSON.
|
||||||
|
func (suite *StatusStandardTestSuite) parseStatusResponse(
|
||||||
|
recorder *httptest.ResponseRecorder,
|
||||||
|
) (string, *httptest.ResponseRecorder) {
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(result.Body)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMap := make(map[string]any)
|
||||||
|
if err := json.Unmarshal(data, &rawMap); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make status fields determinate.
|
||||||
|
suite.determinateStatus(rawMap)
|
||||||
|
|
||||||
|
// For readability, don't
|
||||||
|
// escape HTML, and indent json.
|
||||||
|
out := new(bytes.Buffer)
|
||||||
|
enc := json.NewEncoder(out)
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
|
||||||
|
if err := enc.Encode(&rawMap); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(out.String()), recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *StatusStandardTestSuite) determinateStatus(rawMap map[string]any) {
|
||||||
|
// Replace any fields from the raw map that
|
||||||
|
// aren't determinate (date, id, url, etc).
|
||||||
|
if _, ok := rawMap["id"]; ok {
|
||||||
|
rawMap["id"] = id.Highest
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := rawMap["uri"]; ok {
|
||||||
|
rawMap["uri"] = "http://localhost:8080/some/determinate/url"
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := rawMap["url"]; ok {
|
||||||
|
rawMap["url"] = "http://localhost:8080/some/determinate/url"
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := rawMap["created_at"]; ok {
|
||||||
|
rawMap["created_at"] = "right the hell just now babyee"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make ID of any mentions determinate.
|
||||||
|
if menchiesRaw, ok := rawMap["mentions"]; ok {
|
||||||
|
menchies, ok := menchiesRaw.([]any)
|
||||||
|
if !ok {
|
||||||
|
suite.FailNow("couldn't coerce menchies")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, menchieRaw := range menchies {
|
||||||
|
menchie, ok := menchieRaw.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
suite.FailNow("couldn't coerce menchie")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := menchie["id"]; ok {
|
||||||
|
menchie["id"] = id.Highest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make fields of any poll determinate.
|
||||||
|
if pollRaw, ok := rawMap["poll"]; ok && pollRaw != nil {
|
||||||
|
poll, ok := pollRaw.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
suite.FailNow("couldn't coerce poll")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := poll["id"]; ok {
|
||||||
|
poll["id"] = id.Highest
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := poll["expires_at"]; ok {
|
||||||
|
poll["expires_at"] = "ah like you know whatever dude it's chill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace account since that's not really
|
||||||
|
// what we care about for these tests.
|
||||||
|
if _, ok := rawMap["account"]; ok {
|
||||||
|
rawMap["account"] = "yeah this is my account, what about it punk"
|
||||||
|
}
|
||||||
|
|
||||||
|
// If status contains an embedded
|
||||||
|
// reblog do the same thing for that.
|
||||||
|
if reblogRaw, ok := rawMap["reblog"]; ok && reblogRaw != nil {
|
||||||
|
reblog, ok := reblogRaw.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
suite.FailNow("couldn't coerce reblog")
|
||||||
|
}
|
||||||
|
suite.determinateStatus(reblog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *StatusStandardTestSuite) SetupSuite() {
|
func (suite *StatusStandardTestSuite) SetupSuite() {
|
||||||
suite.testTokens = testrig.NewTestTokens()
|
suite.testTokens = testrig.NewTestTokens()
|
||||||
suite.testClients = testrig.NewTestClients()
|
suite.testClients = testrig.NewTestClients()
|
||||||
|
|
|
@ -17,9 +17,6 @@ package statuses_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -28,7 +25,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
@ -38,212 +35,596 @@ type StatusBoostTestSuite struct {
|
||||||
StatusStandardTestSuite
|
StatusStandardTestSuite
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *StatusBoostTestSuite) TestPostBoost() {
|
func (suite *StatusBoostTestSuite) postStatusBoost(
|
||||||
t := suite.testTokens["local_account_1"]
|
targetStatusID string,
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
app *gtsmodel.Application,
|
||||||
|
token *gtsmodel.Token,
|
||||||
targetStatus := suite.testStatuses["admin_account_status_1"]
|
user *gtsmodel.User,
|
||||||
|
account *gtsmodel.Account,
|
||||||
// setup
|
) (string, *httptest.ResponseRecorder) {
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
ctx.Set(oauth.SessionAuthorizedApplication, app)
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(token))
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedUser, user)
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedAccount, account)
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
|
||||||
|
const pathBase = "http://localhost:8080/api" + statuses.ReblogPath
|
||||||
|
path := strings.ReplaceAll(pathBase, ":"+apiutil.IDKey, targetStatusID)
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, path, nil)
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
// normally the router would populate these params from the path values,
|
// Populate target status ID.
|
||||||
// but because we're calling the function directly, we need to set them manually.
|
|
||||||
ctx.Params = gin.Params{
|
ctx.Params = gin.Params{
|
||||||
gin.Param{
|
gin.Param{
|
||||||
Key: statuses.IDKey,
|
Key: apiutil.IDKey,
|
||||||
Value: targetStatus.ID,
|
Value: targetStatusID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger handler.
|
||||||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
||||||
|
return suite.parseStatusResponse(recorder)
|
||||||
|
}
|
||||||
|
|
||||||
// check response
|
func (suite *StatusBoostTestSuite) TestPostBoost() {
|
||||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
var (
|
||||||
|
targetStatus = suite.testStatuses["admin_account_status_1"]
|
||||||
|
app = suite.testApplications["application_1"]
|
||||||
|
token = suite.testTokens["local_account_1"]
|
||||||
|
user = suite.testUsers["local_account_1"]
|
||||||
|
account = suite.testAccounts["local_account_1"]
|
||||||
|
)
|
||||||
|
|
||||||
result := recorder.Result()
|
out, recorder := suite.postStatusBoost(
|
||||||
defer result.Body.Close()
|
targetStatus.ID,
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
app,
|
||||||
suite.NoError(err)
|
token,
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
statusReply := &apimodel.Status{}
|
// We should have OK from
|
||||||
err = json.Unmarshal(b, statusReply)
|
// our call to the function.
|
||||||
suite.NoError(err)
|
suite.Equal(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
suite.False(statusReply.Sensitive)
|
// Target status should now
|
||||||
suite.Equal(apimodel.VisibilityPublic, statusReply.Visibility)
|
// be "reblogged" by us.
|
||||||
|
suite.Equal(`{
|
||||||
suite.Empty(statusReply.SpoilerText)
|
"account": "yeah this is my account, what about it punk",
|
||||||
suite.Empty(statusReply.Content)
|
"application": {
|
||||||
suite.Equal("the_mighty_zork", statusReply.Account.Username)
|
"name": "really cool gts application",
|
||||||
suite.Len(statusReply.MediaAttachments, 0)
|
"website": "https://reallycool.app"
|
||||||
suite.Len(statusReply.Mentions, 0)
|
},
|
||||||
suite.Len(statusReply.Emojis, 0)
|
"bookmarked": true,
|
||||||
suite.Len(statusReply.Tags, 0)
|
"card": null,
|
||||||
|
"content": "",
|
||||||
suite.NotNil(statusReply.Application)
|
"created_at": "right the hell just now babyee",
|
||||||
suite.Equal("really cool gts application", statusReply.Application.Name)
|
"emojis": [],
|
||||||
|
"favourited": true,
|
||||||
suite.NotNil(statusReply.Reblog)
|
"favourites_count": 0,
|
||||||
suite.Equal(1, statusReply.Reblog.ReblogsCount)
|
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||||
suite.Equal(1, statusReply.Reblog.FavouritesCount)
|
"in_reply_to_account_id": null,
|
||||||
suite.Equal(targetStatus.Content, statusReply.Reblog.Content)
|
"in_reply_to_id": null,
|
||||||
suite.Equal(targetStatus.ContentWarning, statusReply.Reblog.SpoilerText)
|
"interaction_policy": {
|
||||||
suite.Equal(targetStatus.AccountID, statusReply.Reblog.Account.ID)
|
"can_favourite": {
|
||||||
suite.Len(statusReply.Reblog.MediaAttachments, 1)
|
"always": [
|
||||||
suite.Len(statusReply.Reblog.Tags, 1)
|
"public",
|
||||||
suite.Len(statusReply.Reblog.Emojis, 1)
|
"me"
|
||||||
suite.True(statusReply.Reblogged)
|
],
|
||||||
suite.True(statusReply.Reblog.Reblogged)
|
"with_approval": []
|
||||||
suite.Equal("superseriousbusiness", statusReply.Reblog.Application.Name)
|
},
|
||||||
|
"can_reblog": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reply": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": null,
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"poll": null,
|
||||||
|
"reblog": {
|
||||||
|
"account": "yeah this is my account, what about it punk",
|
||||||
|
"application": {
|
||||||
|
"name": "superseriousbusiness",
|
||||||
|
"website": "https://superserious.business"
|
||||||
|
},
|
||||||
|
"bookmarked": true,
|
||||||
|
"card": null,
|
||||||
|
"content": "hello world! #welcome ! first post on the instance :rainbow: !",
|
||||||
|
"created_at": "right the hell just now babyee",
|
||||||
|
"emojis": [
|
||||||
|
{
|
||||||
|
"category": "reactions",
|
||||||
|
"shortcode": "rainbow",
|
||||||
|
"static_url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png",
|
||||||
|
"url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png",
|
||||||
|
"visible_in_picker": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"favourited": true,
|
||||||
|
"favourites_count": 1,
|
||||||
|
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
|
"in_reply_to_id": null,
|
||||||
|
"interaction_policy": {
|
||||||
|
"can_favourite": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reblog": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reply": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": "en",
|
||||||
|
"media_attachments": [
|
||||||
|
{
|
||||||
|
"blurhash": "LIIE|gRj00WB-;j[t7j[4nWBj[Rj",
|
||||||
|
"description": "Black and white image of some 50's style text saying: Welcome On Board",
|
||||||
|
"id": "01F8MH6NEM8D7527KZAECTCR76",
|
||||||
|
"meta": {
|
||||||
|
"focus": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"aspect": 1.9047619,
|
||||||
|
"height": 630,
|
||||||
|
"size": "1200x630",
|
||||||
|
"width": 1200
|
||||||
|
},
|
||||||
|
"small": {
|
||||||
|
"aspect": 1.9104477,
|
||||||
|
"height": 268,
|
||||||
|
"size": "512x268",
|
||||||
|
"width": 512
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preview_remote_url": null,
|
||||||
|
"preview_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.webp",
|
||||||
|
"remote_url": null,
|
||||||
|
"text_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg",
|
||||||
|
"type": "image",
|
||||||
|
"url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mentions": [],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"poll": null,
|
||||||
|
"reblog": null,
|
||||||
|
"reblogged": true,
|
||||||
|
"reblogs_count": 1,
|
||||||
|
"replies_count": 1,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "welcome",
|
||||||
|
"url": "http://localhost:8080/tags/welcome"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "hello world! #welcome ! first post on the instance :rainbow: !",
|
||||||
|
"uri": "http://localhost:8080/some/determinate/url",
|
||||||
|
"url": "http://localhost:8080/some/determinate/url",
|
||||||
|
"visibility": "public"
|
||||||
|
},
|
||||||
|
"reblogged": true,
|
||||||
|
"reblogs_count": 0,
|
||||||
|
"replies_count": 0,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"tags": [],
|
||||||
|
"uri": "http://localhost:8080/some/determinate/url",
|
||||||
|
"url": "http://localhost:8080/some/determinate/url",
|
||||||
|
"visibility": "public"
|
||||||
|
}`, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() {
|
func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() {
|
||||||
t := suite.testTokens["local_account_1"]
|
var (
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
targetStatus = suite.testStatuses["local_account_1_status_5"]
|
||||||
|
app = suite.testApplications["application_1"]
|
||||||
|
token = suite.testTokens["local_account_1"]
|
||||||
|
user = suite.testUsers["local_account_1"]
|
||||||
|
account = suite.testAccounts["local_account_1"]
|
||||||
|
)
|
||||||
|
|
||||||
testStatus := suite.testStatuses["local_account_1_status_5"]
|
out, recorder := suite.postStatusBoost(
|
||||||
testAccount := suite.testAccounts["local_account_1"]
|
targetStatus.ID,
|
||||||
testUser := suite.testUsers["local_account_1"]
|
app,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
// We should have OK from
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
// our call to the function.
|
||||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
suite.Equal(http.StatusOK, recorder.Code)
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, testUser)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, testAccount)
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", testStatus.ID, 1)), nil)
|
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
|
||||||
|
|
||||||
ctx.Params = gin.Params{
|
// Target status should now
|
||||||
gin.Param{
|
// be "reblogged" by us.
|
||||||
Key: statuses.IDKey,
|
suite.Equal(`{
|
||||||
Value: testStatus.ID,
|
"account": "yeah this is my account, what about it punk",
|
||||||
},
|
"application": {
|
||||||
}
|
"name": "really cool gts application",
|
||||||
|
"website": "https://reallycool.app"
|
||||||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
},
|
||||||
|
"bookmarked": false,
|
||||||
// check response
|
"card": null,
|
||||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
"content": "",
|
||||||
|
"created_at": "right the hell just now babyee",
|
||||||
result := recorder.Result()
|
"emojis": [],
|
||||||
defer result.Body.Close()
|
"favourited": false,
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
"favourites_count": 0,
|
||||||
suite.NoError(err)
|
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
responseStatus := &apimodel.Status{}
|
"in_reply_to_id": null,
|
||||||
err = json.Unmarshal(b, responseStatus)
|
"interaction_policy": {
|
||||||
suite.NoError(err)
|
"can_favourite": {
|
||||||
|
"always": [
|
||||||
suite.False(responseStatus.Sensitive)
|
"author",
|
||||||
suite.Equal(suite.tc.VisToAPIVis(context.Background(), testStatus.Visibility), responseStatus.Visibility)
|
"followers",
|
||||||
|
"mentioned",
|
||||||
suite.Empty(responseStatus.SpoilerText)
|
"me"
|
||||||
suite.Empty(responseStatus.Content)
|
],
|
||||||
suite.Equal("the_mighty_zork", responseStatus.Account.Username)
|
"with_approval": []
|
||||||
suite.Len(responseStatus.MediaAttachments, 0)
|
},
|
||||||
suite.Len(responseStatus.Mentions, 0)
|
"can_reblog": {
|
||||||
suite.Len(responseStatus.Emojis, 0)
|
"always": [
|
||||||
suite.Len(responseStatus.Tags, 0)
|
"author",
|
||||||
|
"me"
|
||||||
suite.NotNil(responseStatus.Application)
|
],
|
||||||
suite.Equal("really cool gts application", responseStatus.Application.Name)
|
"with_approval": []
|
||||||
|
},
|
||||||
suite.NotNil(responseStatus.Reblog)
|
"can_reply": {
|
||||||
suite.Equal(1, responseStatus.Reblog.ReblogsCount)
|
"always": [
|
||||||
suite.Equal(0, responseStatus.Reblog.FavouritesCount)
|
"author",
|
||||||
suite.Equal(testStatus.Content, responseStatus.Reblog.Content)
|
"followers",
|
||||||
suite.Equal(testStatus.ContentWarning, responseStatus.Reblog.SpoilerText)
|
"mentioned",
|
||||||
suite.Equal(testStatus.AccountID, responseStatus.Reblog.Account.ID)
|
"me"
|
||||||
suite.Equal(suite.tc.VisToAPIVis(context.Background(), testStatus.Visibility), responseStatus.Reblog.Visibility)
|
],
|
||||||
suite.Empty(responseStatus.Reblog.MediaAttachments)
|
"with_approval": []
|
||||||
suite.Empty(responseStatus.Reblog.Tags)
|
}
|
||||||
suite.Empty(responseStatus.Reblog.Emojis)
|
},
|
||||||
suite.True(responseStatus.Reblogged)
|
"language": null,
|
||||||
suite.True(responseStatus.Reblog.Reblogged)
|
"media_attachments": [],
|
||||||
suite.Equal("really cool gts application", responseStatus.Reblog.Application.Name)
|
"mentions": [],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"poll": null,
|
||||||
|
"reblog": {
|
||||||
|
"account": "yeah this is my account, what about it punk",
|
||||||
|
"application": {
|
||||||
|
"name": "really cool gts application",
|
||||||
|
"website": "https://reallycool.app"
|
||||||
|
},
|
||||||
|
"bookmarked": false,
|
||||||
|
"card": null,
|
||||||
|
"content": "hi!",
|
||||||
|
"created_at": "right the hell just now babyee",
|
||||||
|
"emojis": [],
|
||||||
|
"favourited": false,
|
||||||
|
"favourites_count": 0,
|
||||||
|
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
|
"in_reply_to_id": null,
|
||||||
|
"interaction_policy": {
|
||||||
|
"can_favourite": {
|
||||||
|
"always": [
|
||||||
|
"author",
|
||||||
|
"followers",
|
||||||
|
"mentioned",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reblog": {
|
||||||
|
"always": [
|
||||||
|
"author",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reply": {
|
||||||
|
"always": [
|
||||||
|
"author",
|
||||||
|
"followers",
|
||||||
|
"mentioned",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": "en",
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"poll": null,
|
||||||
|
"reblog": null,
|
||||||
|
"reblogged": true,
|
||||||
|
"reblogs_count": 1,
|
||||||
|
"replies_count": 0,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"tags": [],
|
||||||
|
"text": "hi!",
|
||||||
|
"uri": "http://localhost:8080/some/determinate/url",
|
||||||
|
"url": "http://localhost:8080/some/determinate/url",
|
||||||
|
"visibility": "private"
|
||||||
|
},
|
||||||
|
"reblogged": true,
|
||||||
|
"reblogs_count": 0,
|
||||||
|
"replies_count": 0,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"tags": [],
|
||||||
|
"uri": "http://localhost:8080/some/determinate/url",
|
||||||
|
"url": "http://localhost:8080/some/determinate/url",
|
||||||
|
"visibility": "private"
|
||||||
|
}`, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to boost a status that's not boostable / visible to us
|
// Try to boost a status that's
|
||||||
|
// not boostable / visible to us.
|
||||||
func (suite *StatusBoostTestSuite) TestPostUnboostable() {
|
func (suite *StatusBoostTestSuite) TestPostUnboostable() {
|
||||||
t := suite.testTokens["local_account_1"]
|
var (
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
targetStatus = suite.testStatuses["local_account_2_status_4"]
|
||||||
|
app = suite.testApplications["application_1"]
|
||||||
|
token = suite.testTokens["local_account_1"]
|
||||||
|
user = suite.testUsers["local_account_1"]
|
||||||
|
account = suite.testAccounts["local_account_1"]
|
||||||
|
)
|
||||||
|
|
||||||
targetStatus := suite.testStatuses["local_account_2_status_4"]
|
out, recorder := suite.postStatusBoost(
|
||||||
|
targetStatus.ID,
|
||||||
|
app,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
// setup
|
// We should have 403 from
|
||||||
recorder := httptest.NewRecorder()
|
// our call to the function.
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
|
||||||
|
|
||||||
// normally the router would populate these params from the path values,
|
|
||||||
// but because we're calling the function directly, we need to set them manually.
|
|
||||||
ctx.Params = gin.Params{
|
|
||||||
gin.Param{
|
|
||||||
Key: statuses.IDKey,
|
|
||||||
Value: targetStatus.ID,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
|
||||||
|
|
||||||
// check response
|
|
||||||
suite.Equal(http.StatusForbidden, recorder.Code)
|
suite.Equal(http.StatusForbidden, recorder.Code)
|
||||||
|
|
||||||
result := recorder.Result()
|
// We should have a helpful message.
|
||||||
defer result.Body.Close()
|
suite.Equal(`{
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
"error": "Forbidden: you do not have permission to boost this status"
|
||||||
suite.NoError(err)
|
}`, out)
|
||||||
suite.Equal(`{"error":"Forbidden: you do not have permission to boost this status"}`, string(b))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to boost a status that's not visible to the user
|
// Try to boost a status that's not visible to the user.
|
||||||
func (suite *StatusBoostTestSuite) TestPostNotVisible() {
|
func (suite *StatusBoostTestSuite) TestPostNotVisible() {
|
||||||
// stop local_account_2 following zork
|
// Stop local_account_2 following zork.
|
||||||
err := suite.db.DeleteByID(context.Background(), suite.testFollows["local_account_2_local_account_1"].ID, >smodel.Follow{})
|
err := suite.db.DeleteFollowByID(
|
||||||
suite.NoError(err)
|
context.Background(),
|
||||||
|
suite.testFollows["local_account_2_local_account_1"].ID,
|
||||||
t := suite.testTokens["local_account_2"]
|
)
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
targetStatus := suite.testStatuses["local_account_1_status_3"] // this is a mutual only status and these accounts aren't mutuals
|
|
||||||
|
|
||||||
// setup
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_2"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_2"])
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
|
||||||
|
|
||||||
// normally the router would populate these params from the path values,
|
|
||||||
// but because we're calling the function directly, we need to set them manually.
|
|
||||||
ctx.Params = gin.Params{
|
|
||||||
gin.Param{
|
|
||||||
Key: statuses.IDKey,
|
|
||||||
Value: targetStatus.ID,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
var (
|
||||||
|
// This is a mutual only status and
|
||||||
|
// these accounts aren't mutuals anymore.
|
||||||
|
targetStatus = suite.testStatuses["local_account_1_status_3"]
|
||||||
|
app = suite.testApplications["application_1"]
|
||||||
|
token = suite.testTokens["local_account_2"]
|
||||||
|
user = suite.testUsers["local_account_2"]
|
||||||
|
account = suite.testAccounts["local_account_2"]
|
||||||
|
)
|
||||||
|
|
||||||
// check response
|
out, recorder := suite.postStatusBoost(
|
||||||
suite.Equal(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible
|
targetStatus.ID,
|
||||||
|
app,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We should have 404 from
|
||||||
|
// our call to the function.
|
||||||
|
suite.Equal(http.StatusNotFound, recorder.Code)
|
||||||
|
|
||||||
|
// We should have a helpful message.
|
||||||
|
suite.Equal(`{
|
||||||
|
"error": "Not Found: target status not found"
|
||||||
|
}`, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boost a status that's pending approval by us.
|
||||||
|
func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() {
|
||||||
|
var (
|
||||||
|
targetStatus = suite.testStatuses["admin_account_status_5"]
|
||||||
|
app = suite.testApplications["application_1"]
|
||||||
|
token = suite.testTokens["local_account_2"]
|
||||||
|
user = suite.testUsers["local_account_2"]
|
||||||
|
account = suite.testAccounts["local_account_2"]
|
||||||
|
)
|
||||||
|
|
||||||
|
out, recorder := suite.postStatusBoost(
|
||||||
|
targetStatus.ID,
|
||||||
|
app,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We should have OK from
|
||||||
|
// our call to the function.
|
||||||
|
suite.Equal(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
// Target status should now
|
||||||
|
// be "reblogged" by us.
|
||||||
|
suite.Equal(`{
|
||||||
|
"account": "yeah this is my account, what about it punk",
|
||||||
|
"application": {
|
||||||
|
"name": "really cool gts application",
|
||||||
|
"website": "https://reallycool.app"
|
||||||
|
},
|
||||||
|
"bookmarked": false,
|
||||||
|
"card": null,
|
||||||
|
"content": "",
|
||||||
|
"created_at": "right the hell just now babyee",
|
||||||
|
"emojis": [],
|
||||||
|
"favourited": false,
|
||||||
|
"favourites_count": 0,
|
||||||
|
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
|
"in_reply_to_id": null,
|
||||||
|
"interaction_policy": {
|
||||||
|
"can_favourite": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reblog": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reply": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": null,
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"poll": null,
|
||||||
|
"reblog": {
|
||||||
|
"account": "yeah this is my account, what about it punk",
|
||||||
|
"application": {
|
||||||
|
"name": "superseriousbusiness",
|
||||||
|
"website": "https://superserious.business"
|
||||||
|
},
|
||||||
|
"bookmarked": false,
|
||||||
|
"card": null,
|
||||||
|
"content": "<p>Hi <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span>, can I reply?</p>",
|
||||||
|
"created_at": "right the hell just now babyee",
|
||||||
|
"emojis": [],
|
||||||
|
"favourited": false,
|
||||||
|
"favourites_count": 0,
|
||||||
|
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||||
|
"in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
|
"in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||||
|
"interaction_policy": {
|
||||||
|
"can_favourite": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reblog": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reply": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": null,
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [
|
||||||
|
{
|
||||||
|
"acct": "1happyturtle",
|
||||||
|
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||||
|
"url": "http://localhost:8080/@1happyturtle",
|
||||||
|
"username": "1happyturtle"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"poll": null,
|
||||||
|
"reblog": null,
|
||||||
|
"reblogged": true,
|
||||||
|
"reblogs_count": 1,
|
||||||
|
"replies_count": 0,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"tags": [],
|
||||||
|
"text": "Hi @1happyturtle, can I reply?",
|
||||||
|
"uri": "http://localhost:8080/some/determinate/url",
|
||||||
|
"url": "http://localhost:8080/some/determinate/url",
|
||||||
|
"visibility": "unlisted"
|
||||||
|
},
|
||||||
|
"reblogged": true,
|
||||||
|
"reblogs_count": 0,
|
||||||
|
"replies_count": 0,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"tags": [],
|
||||||
|
"uri": "http://localhost:8080/some/determinate/url",
|
||||||
|
"url": "http://localhost:8080/some/determinate/url",
|
||||||
|
"visibility": "unlisted"
|
||||||
|
}`, out)
|
||||||
|
|
||||||
|
// Target status should no
|
||||||
|
// longer be pending approval.
|
||||||
|
dbStatus, err := suite.state.DB.GetStatusByID(
|
||||||
|
context.Background(),
|
||||||
|
targetStatus.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
suite.False(*dbStatus.PendingApproval)
|
||||||
|
|
||||||
|
// There should be an Accept
|
||||||
|
// stored for the target status.
|
||||||
|
intReq, err := suite.state.DB.GetInteractionRequestByInteractionURI(
|
||||||
|
context.Background(), targetStatus.URI,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
suite.NotZero(intReq.AcceptedAt)
|
||||||
|
suite.NotEmpty(intReq.URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusBoostTestSuite(t *testing.T) {
|
func TestStatusBoostTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -20,18 +20,14 @@ package statuses_test
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
@ -81,91 +77,7 @@ func (suite *StatusCreateTestSuite) postStatus(
|
||||||
|
|
||||||
// Trigger handler.
|
// Trigger handler.
|
||||||
suite.statusModule.StatusCreatePOSTHandler(ctx)
|
suite.statusModule.StatusCreatePOSTHandler(ctx)
|
||||||
|
return suite.parseStatusResponse(recorder)
|
||||||
result := recorder.Result()
|
|
||||||
defer result.Body.Close()
|
|
||||||
|
|
||||||
data, err := io.ReadAll(result.Body)
|
|
||||||
if err != nil {
|
|
||||||
suite.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
rawMap := make(map[string]any)
|
|
||||||
if err := json.Unmarshal(data, &rawMap); err != nil {
|
|
||||||
suite.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace any fields from the raw map that
|
|
||||||
// aren't determinate (date, id, url, etc).
|
|
||||||
if _, ok := rawMap["id"]; ok {
|
|
||||||
rawMap["id"] = id.Highest
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := rawMap["uri"]; ok {
|
|
||||||
rawMap["uri"] = "http://localhost:8080/some/determinate/url"
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := rawMap["url"]; ok {
|
|
||||||
rawMap["url"] = "http://localhost:8080/some/determinate/url"
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := rawMap["created_at"]; ok {
|
|
||||||
rawMap["created_at"] = "right the hell just now babyee"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make ID of any mentions determinate.
|
|
||||||
if menchiesRaw, ok := rawMap["mentions"]; ok {
|
|
||||||
menchies, ok := menchiesRaw.([]any)
|
|
||||||
if !ok {
|
|
||||||
suite.FailNow("couldn't coerce menchies")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, menchieRaw := range menchies {
|
|
||||||
menchie, ok := menchieRaw.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
suite.FailNow("couldn't coerce menchie")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := menchie["id"]; ok {
|
|
||||||
menchie["id"] = id.Highest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make fields of any poll determinate.
|
|
||||||
if pollRaw, ok := rawMap["poll"]; ok && pollRaw != nil {
|
|
||||||
poll, ok := pollRaw.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
suite.FailNow("couldn't coerce poll")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := poll["id"]; ok {
|
|
||||||
poll["id"] = id.Highest
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := poll["expires_at"]; ok {
|
|
||||||
poll["expires_at"] = "ah like you know whatever dude it's chill"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace account since that's not really
|
|
||||||
// what we care about for these tests.
|
|
||||||
if _, ok := rawMap["account"]; ok {
|
|
||||||
rawMap["account"] = "yeah this is my account, what about it punk"
|
|
||||||
}
|
|
||||||
|
|
||||||
// For readability, don't
|
|
||||||
// escape HTML, and indent json.
|
|
||||||
out := new(bytes.Buffer)
|
|
||||||
enc := json.NewEncoder(out)
|
|
||||||
enc.SetEscapeHTML(false)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
|
|
||||||
if err := enc.Encode(&rawMap); err != nil {
|
|
||||||
suite.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimSpace(out.String()), recorder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post a new status with some custom visibility settings
|
// Post a new status with some custom visibility settings
|
||||||
|
@ -447,7 +359,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMessedUpIntPolicy() {
|
||||||
suite.Equal(http.StatusBadRequest, recorder.Code)
|
suite.Equal(http.StatusBadRequest, recorder.Code)
|
||||||
|
|
||||||
// We should have a helpful error
|
// We should have a helpful error
|
||||||
// message telling us how we screwed up.
|
// message telling us how we screwed up.
|
||||||
suite.Equal(`{
|
suite.Equal(`{
|
||||||
"error": "Bad Request: error converting followers_only.can_reply.always: policyURI public is not feasible for visibility followers_only"
|
"error": "Bad Request: error converting followers_only.can_reply.always: policyURI public is not feasible for visibility followers_only"
|
||||||
}`, out)
|
}`, out)
|
||||||
|
|
|
@ -18,20 +18,18 @@
|
||||||
package statuses_test
|
package statuses_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
@ -40,90 +38,260 @@ type StatusFaveTestSuite struct {
|
||||||
StatusStandardTestSuite
|
StatusStandardTestSuite
|
||||||
}
|
}
|
||||||
|
|
||||||
// fave a status
|
func (suite *StatusFaveTestSuite) postStatusFave(
|
||||||
func (suite *StatusFaveTestSuite) TestPostFave() {
|
targetStatusID string,
|
||||||
t := suite.testTokens["local_account_1"]
|
app *gtsmodel.Application,
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
token *gtsmodel.Token,
|
||||||
|
user *gtsmodel.User,
|
||||||
targetStatus := suite.testStatuses["admin_account_status_2"]
|
account *gtsmodel.Account,
|
||||||
|
) (string, *httptest.ResponseRecorder) {
|
||||||
// setup
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
ctx.Set(oauth.SessionAuthorizedApplication, app)
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(token))
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedUser, user)
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedAccount, account)
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
|
||||||
|
const pathBase = "http://localhost:8080/api" + statuses.FavouritePath
|
||||||
|
path := strings.ReplaceAll(pathBase, ":"+apiutil.IDKey, targetStatusID)
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, path, nil)
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
// normally the router would populate these params from the path values,
|
// Populate target status ID.
|
||||||
// but because we're calling the function directly, we need to set them manually.
|
|
||||||
ctx.Params = gin.Params{
|
ctx.Params = gin.Params{
|
||||||
gin.Param{
|
gin.Param{
|
||||||
Key: statuses.IDKey,
|
Key: apiutil.IDKey,
|
||||||
Value: targetStatus.ID,
|
Value: targetStatusID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger handler.
|
||||||
suite.statusModule.StatusFavePOSTHandler(ctx)
|
suite.statusModule.StatusFavePOSTHandler(ctx)
|
||||||
|
return suite.parseStatusResponse(recorder)
|
||||||
// check response
|
|
||||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
|
||||||
|
|
||||||
result := recorder.Result()
|
|
||||||
defer result.Body.Close()
|
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
|
||||||
assert.NoError(suite.T(), err)
|
|
||||||
|
|
||||||
statusReply := &apimodel.Status{}
|
|
||||||
err = json.Unmarshal(b, statusReply)
|
|
||||||
assert.NoError(suite.T(), err)
|
|
||||||
|
|
||||||
assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText)
|
|
||||||
assert.Equal(suite.T(), targetStatus.Content, statusReply.Content)
|
|
||||||
assert.True(suite.T(), statusReply.Sensitive)
|
|
||||||
assert.Equal(suite.T(), apimodel.VisibilityPublic, statusReply.Visibility)
|
|
||||||
assert.True(suite.T(), statusReply.Favourited)
|
|
||||||
assert.Equal(suite.T(), 1, statusReply.FavouritesCount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to fave a status that's not faveable
|
// Fave a status we haven't faved yet.
|
||||||
|
func (suite *StatusFaveTestSuite) TestPostFave() {
|
||||||
|
var (
|
||||||
|
targetStatus = suite.testStatuses["admin_account_status_2"]
|
||||||
|
app = suite.testApplications["application_1"]
|
||||||
|
token = suite.testTokens["local_account_1"]
|
||||||
|
user = suite.testUsers["local_account_1"]
|
||||||
|
account = suite.testAccounts["local_account_1"]
|
||||||
|
)
|
||||||
|
|
||||||
|
out, recorder := suite.postStatusFave(
|
||||||
|
targetStatus.ID,
|
||||||
|
app,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We should have OK from
|
||||||
|
// our call to the function.
|
||||||
|
suite.Equal(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
// Target status should now
|
||||||
|
// be "favourited" by us.
|
||||||
|
suite.Equal(`{
|
||||||
|
"account": "yeah this is my account, what about it punk",
|
||||||
|
"application": {
|
||||||
|
"name": "superseriousbusiness",
|
||||||
|
"website": "https://superserious.business"
|
||||||
|
},
|
||||||
|
"bookmarked": false,
|
||||||
|
"card": null,
|
||||||
|
"content": "🐕🐕🐕🐕🐕",
|
||||||
|
"created_at": "right the hell just now babyee",
|
||||||
|
"emojis": [],
|
||||||
|
"favourited": true,
|
||||||
|
"favourites_count": 1,
|
||||||
|
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
|
"in_reply_to_id": null,
|
||||||
|
"interaction_policy": {
|
||||||
|
"can_favourite": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reblog": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reply": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": "en",
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"poll": null,
|
||||||
|
"reblog": null,
|
||||||
|
"reblogged": false,
|
||||||
|
"reblogs_count": 0,
|
||||||
|
"replies_count": 0,
|
||||||
|
"sensitive": true,
|
||||||
|
"spoiler_text": "open to see some puppies",
|
||||||
|
"tags": [],
|
||||||
|
"text": "🐕🐕🐕🐕🐕",
|
||||||
|
"uri": "http://localhost:8080/some/determinate/url",
|
||||||
|
"url": "http://localhost:8080/some/determinate/url",
|
||||||
|
"visibility": "public"
|
||||||
|
}`, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to fave a status
|
||||||
|
// that's not faveable by us.
|
||||||
func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
||||||
t := suite.testTokens["admin_account"]
|
var (
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
targetStatus = suite.testStatuses["local_account_1_status_3"]
|
||||||
|
app = suite.testApplications["application_1"]
|
||||||
|
token = suite.testTokens["admin_account"]
|
||||||
|
user = suite.testUsers["admin_account"]
|
||||||
|
account = suite.testAccounts["admin_account"]
|
||||||
|
)
|
||||||
|
|
||||||
targetStatus := suite.testStatuses["local_account_1_status_3"] // this one is unlikeable
|
out, recorder := suite.postStatusFave(
|
||||||
|
targetStatus.ID,
|
||||||
|
app,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
// setup
|
// We should have 403 from
|
||||||
recorder := httptest.NewRecorder()
|
// our call to the function.
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
suite.Equal(http.StatusForbidden, recorder.Code)
|
||||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["admin_account"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["admin_account"])
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
|
||||||
|
|
||||||
// normally the router would populate these params from the path values,
|
// We should get a helpful error.
|
||||||
// but because we're calling the function directly, we need to set them manually.
|
suite.Equal(`{
|
||||||
ctx.Params = gin.Params{
|
"error": "Forbidden: you do not have permission to fave this status"
|
||||||
gin.Param{
|
}`, out)
|
||||||
Key: statuses.IDKey,
|
}
|
||||||
Value: targetStatus.ID,
|
|
||||||
},
|
// Fave a status that's pending approval by us.
|
||||||
|
func (suite *StatusFaveTestSuite) TestPostFaveImplicitAccept() {
|
||||||
|
var (
|
||||||
|
targetStatus = suite.testStatuses["admin_account_status_5"]
|
||||||
|
app = suite.testApplications["application_1"]
|
||||||
|
token = suite.testTokens["local_account_2"]
|
||||||
|
user = suite.testUsers["local_account_2"]
|
||||||
|
account = suite.testAccounts["local_account_2"]
|
||||||
|
)
|
||||||
|
|
||||||
|
out, recorder := suite.postStatusFave(
|
||||||
|
targetStatus.ID,
|
||||||
|
app,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We should have OK from
|
||||||
|
// our call to the function.
|
||||||
|
suite.Equal(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
// Target status should now
|
||||||
|
// be "favourited" by us.
|
||||||
|
suite.Equal(`{
|
||||||
|
"account": "yeah this is my account, what about it punk",
|
||||||
|
"application": {
|
||||||
|
"name": "superseriousbusiness",
|
||||||
|
"website": "https://superserious.business"
|
||||||
|
},
|
||||||
|
"bookmarked": false,
|
||||||
|
"card": null,
|
||||||
|
"content": "<p>Hi <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span>, can I reply?</p>",
|
||||||
|
"created_at": "right the hell just now babyee",
|
||||||
|
"emojis": [],
|
||||||
|
"favourited": true,
|
||||||
|
"favourites_count": 1,
|
||||||
|
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||||
|
"in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
|
"in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||||
|
"interaction_policy": {
|
||||||
|
"can_favourite": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reblog": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reply": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": null,
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [
|
||||||
|
{
|
||||||
|
"acct": "1happyturtle",
|
||||||
|
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||||
|
"url": "http://localhost:8080/@1happyturtle",
|
||||||
|
"username": "1happyturtle"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muted": false,
|
||||||
|
"pinned": false,
|
||||||
|
"poll": null,
|
||||||
|
"reblog": null,
|
||||||
|
"reblogged": false,
|
||||||
|
"reblogs_count": 0,
|
||||||
|
"replies_count": 0,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"tags": [],
|
||||||
|
"text": "Hi @1happyturtle, can I reply?",
|
||||||
|
"uri": "http://localhost:8080/some/determinate/url",
|
||||||
|
"url": "http://localhost:8080/some/determinate/url",
|
||||||
|
"visibility": "unlisted"
|
||||||
|
}`, out)
|
||||||
|
|
||||||
|
// Target status should no
|
||||||
|
// longer be pending approval.
|
||||||
|
dbStatus, err := suite.state.DB.GetStatusByID(
|
||||||
|
context.Background(),
|
||||||
|
targetStatus.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
suite.False(*dbStatus.PendingApproval)
|
||||||
|
|
||||||
suite.statusModule.StatusFavePOSTHandler(ctx)
|
// There should be an Accept
|
||||||
|
// stored for the target status.
|
||||||
// check response
|
intReq, err := suite.state.DB.GetInteractionRequestByInteractionURI(
|
||||||
suite.EqualValues(http.StatusForbidden, recorder.Code)
|
context.Background(), targetStatus.URI,
|
||||||
|
)
|
||||||
result := recorder.Result()
|
if err != nil {
|
||||||
defer result.Body.Close()
|
suite.FailNow(err.Error())
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
}
|
||||||
assert.NoError(suite.T(), err)
|
suite.NotZero(intReq.AcceptedAt)
|
||||||
assert.Equal(suite.T(), `{"error":"Forbidden: you do not have permission to fave this status"}`, string(b))
|
suite.NotEmpty(intReq.URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusFaveTestSuite(t *testing.T) {
|
func TestStatusFaveTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//go:build !nometrics
|
||||||
|
|
||||||
package metrics
|
package metrics
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//go:build nometrics
|
||||||
|
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Module struct{}
|
||||||
|
|
||||||
|
func New() *Module { return &Module{} }
|
||||||
|
|
||||||
|
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
|
||||||
|
}
|
|
@ -18,6 +18,8 @@
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"codeberg.org/gruf/go-structr"
|
"codeberg.org/gruf/go-structr"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/cache/domain"
|
"github.com/superseriousbusiness/gotosocial/internal/cache/domain"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
@ -136,6 +138,14 @@ type DBCaches struct {
|
||||||
// Instance provides access to the gtsmodel Instance database cache.
|
// Instance provides access to the gtsmodel Instance database cache.
|
||||||
Instance StructCache[*gtsmodel.Instance]
|
Instance StructCache[*gtsmodel.Instance]
|
||||||
|
|
||||||
|
// LocalInstance provides caching for
|
||||||
|
// simple + common local instance queries.
|
||||||
|
LocalInstance struct {
|
||||||
|
Domains atomic.Pointer[int]
|
||||||
|
Statuses atomic.Pointer[int]
|
||||||
|
Users atomic.Pointer[int]
|
||||||
|
}
|
||||||
|
|
||||||
// InteractionRequest provides access to the gtsmodel InteractionRequest database cache.
|
// InteractionRequest provides access to the gtsmodel InteractionRequest database cache.
|
||||||
InteractionRequest StructCache[*gtsmodel.InteractionRequest]
|
InteractionRequest StructCache[*gtsmodel.InteractionRequest]
|
||||||
|
|
||||||
|
@ -849,9 +859,10 @@ func (c *Caches) initInstance() {
|
||||||
{Fields: "ID"},
|
{Fields: "ID"},
|
||||||
{Fields: "Domain"},
|
{Fields: "Domain"},
|
||||||
},
|
},
|
||||||
MaxSize: cap,
|
MaxSize: cap,
|
||||||
IgnoreErr: ignoreErrors,
|
IgnoreErr: ignoreErrors,
|
||||||
Copy: copyF,
|
Copy: copyF,
|
||||||
|
Invalidate: c.OnInvalidateInstance,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Below are cache invalidation hooks between other caches,
|
// Below are cache invalidation hooks between other caches,
|
||||||
|
@ -178,6 +179,11 @@ func (c *Caches) OnInvalidateFollowRequest(followReq *gtsmodel.FollowRequest) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Caches) OnInvalidateInstance(instance *gtsmodel.Instance) {
|
||||||
|
// Invalidate the local domains count.
|
||||||
|
c.DB.LocalInstance.Domains.Store(nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Caches) OnInvalidateList(list *gtsmodel.List) {
|
func (c *Caches) OnInvalidateList(list *gtsmodel.List) {
|
||||||
// Invalidate list IDs cache.
|
// Invalidate list IDs cache.
|
||||||
c.DB.ListIDs.Invalidate(
|
c.DB.ListIDs.Invalidate(
|
||||||
|
@ -255,6 +261,11 @@ func (c *Caches) OnInvalidateStatus(status *gtsmodel.Status) {
|
||||||
// Invalidate cache of attached poll ID.
|
// Invalidate cache of attached poll ID.
|
||||||
c.DB.Poll.Invalidate("ID", status.PollID)
|
c.DB.Poll.Invalidate("ID", status.PollID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if util.PtrOrZero(status.Local) {
|
||||||
|
// Invalidate the local statuses count.
|
||||||
|
c.DB.LocalInstance.Statuses.Store(nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Caches) OnInvalidateStatusBookmark(bookmark *gtsmodel.StatusBookmark) {
|
func (c *Caches) OnInvalidateStatusBookmark(bookmark *gtsmodel.StatusBookmark) {
|
||||||
|
@ -271,6 +282,9 @@ func (c *Caches) OnInvalidateUser(user *gtsmodel.User) {
|
||||||
// Invalidate local account ID cached visibility.
|
// Invalidate local account ID cached visibility.
|
||||||
c.Visibility.Invalidate("ItemID", user.AccountID)
|
c.Visibility.Invalidate("ItemID", user.AccountID)
|
||||||
c.Visibility.Invalidate("RequesterID", user.AccountID)
|
c.Visibility.Invalidate("RequesterID", user.AccountID)
|
||||||
|
|
||||||
|
// Invalidate the local users count.
|
||||||
|
c.DB.LocalInstance.Users.Store(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Caches) OnInvalidateUserMute(mute *gtsmodel.UserMute) {
|
func (c *Caches) OnInvalidateUserMute(mute *gtsmodel.UserMute) {
|
||||||
|
|
|
@ -39,6 +39,15 @@ type instanceDB struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *instanceDB) CountInstanceUsers(ctx context.Context, domain string) (int, error) {
|
func (i *instanceDB) CountInstanceUsers(ctx context.Context, domain string) (int, error) {
|
||||||
|
localhost := (domain == config.GetHost() || domain == config.GetAccountDomain())
|
||||||
|
|
||||||
|
if localhost {
|
||||||
|
// Check for a cached instance user count, if so return this.
|
||||||
|
if n := i.state.Caches.DB.LocalInstance.Users.Load(); n != nil {
|
||||||
|
return *n, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
q := i.db.
|
q := i.db.
|
||||||
NewSelect().
|
NewSelect().
|
||||||
TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
|
TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
|
||||||
|
@ -46,7 +55,7 @@ func (i *instanceDB) CountInstanceUsers(ctx context.Context, domain string) (int
|
||||||
Where("? != ?", bun.Ident("account.username"), domain).
|
Where("? != ?", bun.Ident("account.username"), domain).
|
||||||
Where("? IS NULL", bun.Ident("account.suspended_at"))
|
Where("? IS NULL", bun.Ident("account.suspended_at"))
|
||||||
|
|
||||||
if domain == config.GetHost() || domain == config.GetAccountDomain() {
|
if localhost {
|
||||||
// If the domain is *this* domain, just
|
// If the domain is *this* domain, just
|
||||||
// count where the domain field is null.
|
// count where the domain field is null.
|
||||||
q = q.Where("? IS NULL", bun.Ident("account.domain"))
|
q = q.Where("? IS NULL", bun.Ident("account.domain"))
|
||||||
|
@ -58,15 +67,30 @@ func (i *instanceDB) CountInstanceUsers(ctx context.Context, domain string) (int
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if localhost {
|
||||||
|
// Update cached instance users account value.
|
||||||
|
i.state.Caches.DB.LocalInstance.Users.Store(&count)
|
||||||
|
}
|
||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *instanceDB) CountInstanceStatuses(ctx context.Context, domain string) (int, error) {
|
func (i *instanceDB) CountInstanceStatuses(ctx context.Context, domain string) (int, error) {
|
||||||
|
localhost := (domain == config.GetHost() || domain == config.GetAccountDomain())
|
||||||
|
|
||||||
|
if localhost {
|
||||||
|
// Check for a cached instance statuses count, if so return this.
|
||||||
|
if n := i.state.Caches.DB.LocalInstance.Statuses.Load(); n != nil {
|
||||||
|
return *n, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
q := i.db.
|
q := i.db.
|
||||||
NewSelect().
|
NewSelect().
|
||||||
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status"))
|
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status"))
|
||||||
|
|
||||||
if domain == config.GetHost() || domain == config.GetAccountDomain() {
|
if localhost {
|
||||||
// if the domain is *this* domain, just count where local is true
|
// if the domain is *this* domain, just count where local is true
|
||||||
q = q.Where("? = ?", bun.Ident("status.local"), true)
|
q = q.Where("? = ?", bun.Ident("status.local"), true)
|
||||||
} else {
|
} else {
|
||||||
|
@ -83,15 +107,30 @@ func (i *instanceDB) CountInstanceStatuses(ctx context.Context, domain string) (
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if localhost {
|
||||||
|
// Update cached instance statuses account value.
|
||||||
|
i.state.Caches.DB.LocalInstance.Statuses.Store(&count)
|
||||||
|
}
|
||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *instanceDB) CountInstanceDomains(ctx context.Context, domain string) (int, error) {
|
func (i *instanceDB) CountInstanceDomains(ctx context.Context, domain string) (int, error) {
|
||||||
|
localhost := (domain == config.GetHost() || domain == config.GetAccountDomain())
|
||||||
|
|
||||||
|
if localhost {
|
||||||
|
// Check for a cached instance domains count, if so return this.
|
||||||
|
if n := i.state.Caches.DB.LocalInstance.Domains.Load(); n != nil {
|
||||||
|
return *n, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
q := i.db.
|
q := i.db.
|
||||||
NewSelect().
|
NewSelect().
|
||||||
TableExpr("? AS ?", bun.Ident("instances"), bun.Ident("instance"))
|
TableExpr("? AS ?", bun.Ident("instances"), bun.Ident("instance"))
|
||||||
|
|
||||||
if domain == config.GetHost() {
|
if localhost {
|
||||||
// if the domain is *this* domain, just count other instances it knows about
|
// if the domain is *this* domain, just count other instances it knows about
|
||||||
// exclude domains that are blocked
|
// exclude domains that are blocked
|
||||||
q = q.
|
q = q.
|
||||||
|
@ -106,6 +145,12 @@ func (i *instanceDB) CountInstanceDomains(ctx context.Context, domain string) (i
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if localhost {
|
||||||
|
// Update cached instance domains account value.
|
||||||
|
i.state.Caches.DB.LocalInstance.Domains.Store(&count)
|
||||||
|
}
|
||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,13 +260,15 @@ func (i *instanceDB) PopulateInstance(ctx context.Context, instance *gtsmodel.In
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *instanceDB) PutInstance(ctx context.Context, instance *gtsmodel.Instance) error {
|
func (i *instanceDB) PutInstance(ctx context.Context, instance *gtsmodel.Instance) error {
|
||||||
// Normalize the domain as punycode
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
// Normalize the domain as punycode
|
||||||
instance.Domain, err = util.Punify(instance.Domain)
|
instance.Domain, err = util.Punify(instance.Domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("error punifying domain %s: %w", instance.Domain, err)
|
return gtserror.Newf("error punifying domain %s: %w", instance.Domain, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the new instance model in database, invalidating cache.
|
||||||
return i.state.Caches.DB.Instance.Store(instance, func() error {
|
return i.state.Caches.DB.Instance.Store(instance, func() error {
|
||||||
_, err := i.db.NewInsert().Model(instance).Exec(ctx)
|
_, err := i.db.NewInsert().Model(instance).Exec(ctx)
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -71,17 +71,26 @@ func NewSender() (Sender, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
username := config.GetSMTPUsername()
|
var (
|
||||||
password := config.GetSMTPPassword()
|
username = config.GetSMTPUsername()
|
||||||
host := config.GetSMTPHost()
|
password = config.GetSMTPPassword()
|
||||||
port := config.GetSMTPPort()
|
host = config.GetSMTPHost()
|
||||||
from := config.GetSMTPFrom()
|
port = config.GetSMTPPort()
|
||||||
msgIDHost := config.GetHost()
|
from = config.GetSMTPFrom()
|
||||||
|
msgIDHost = config.GetHost()
|
||||||
|
smtpAuth smtp.Auth
|
||||||
|
)
|
||||||
|
|
||||||
|
if username == "" || password == "" {
|
||||||
|
smtpAuth = nil
|
||||||
|
} else {
|
||||||
|
smtpAuth = smtp.PlainAuth("", username, password, host)
|
||||||
|
}
|
||||||
|
|
||||||
return &sender{
|
return &sender{
|
||||||
hostAddress: fmt.Sprintf("%s:%d", host, port),
|
hostAddress: fmt.Sprintf("%s:%d", host, port),
|
||||||
from: from,
|
from: from,
|
||||||
auth: smtp.PlainAuth("", username, password, host),
|
auth: smtpAuth,
|
||||||
msgIDHost: msgIDHost,
|
msgIDHost: msgIDHost,
|
||||||
template: t,
|
template: t,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
|
@ -104,18 +104,20 @@ func (f *Filter) isStatusVisible(
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if util.PtrOrValue(status.PendingApproval, false) {
|
if util.PtrOrZero(status.PendingApproval) {
|
||||||
// Use a different visibility heuristic
|
// Use a different visibility heuristic
|
||||||
// for pending approval statuses.
|
// for pending approval statuses.
|
||||||
return f.isPendingStatusVisible(ctx,
|
return isPendingStatusVisible(
|
||||||
requester, status,
|
requester, status,
|
||||||
)
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if requester == nil {
|
if requester == nil {
|
||||||
// Use a different visibility
|
// Use a different visibility
|
||||||
// heuristic for unauthed requests.
|
// heuristic for unauthed requests.
|
||||||
return f.isStatusVisibleUnauthed(ctx, status)
|
return f.isStatusVisibleUnauthed(
|
||||||
|
ctx, status,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -210,45 +212,42 @@ func (f *Filter) isStatusVisible(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) isPendingStatusVisible(
|
// isPendingStatusVisible returns whether a status pending approval is visible to requester.
|
||||||
_ context.Context,
|
func isPendingStatusVisible(requester *gtsmodel.Account, status *gtsmodel.Status) bool {
|
||||||
requester *gtsmodel.Account,
|
|
||||||
status *gtsmodel.Status,
|
|
||||||
) (bool, error) {
|
|
||||||
if requester == nil {
|
if requester == nil {
|
||||||
// Any old tom, dick, and harry can't
|
// Any old tom, dick, and harry can't
|
||||||
// see pending-approval statuses,
|
// see pending-approval statuses,
|
||||||
// no matter what their visibility.
|
// no matter what their visibility.
|
||||||
return false, nil
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.AccountID == requester.ID {
|
if status.AccountID == requester.ID {
|
||||||
// This is requester's status,
|
// This is requester's status,
|
||||||
// so they can always see it.
|
// so they can always see it.
|
||||||
return true, nil
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.InReplyToAccountID == requester.ID {
|
if status.InReplyToAccountID == requester.ID {
|
||||||
// This status replies to requester,
|
// This status replies to requester,
|
||||||
// so they can always see it (else
|
// so they can always see it (else
|
||||||
// they can't approve it).
|
// they can't approve it).
|
||||||
return true, nil
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.BoostOfAccountID == requester.ID {
|
if status.BoostOfAccountID == requester.ID {
|
||||||
// This status boosts requester,
|
// This status boosts requester,
|
||||||
// so they can always see it.
|
// so they can always see it.
|
||||||
return true, nil
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nobody else can see this.
|
// Nobody else
|
||||||
return false, nil
|
// can see this.
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Filter) isStatusVisibleUnauthed(
|
// isStatusVisibleUnauthed returns whether status is visible without any unauthenticated account.
|
||||||
ctx context.Context,
|
func (f *Filter) isStatusVisibleUnauthed(ctx context.Context, status *gtsmodel.Status) (bool, error) {
|
||||||
status *gtsmodel.Status,
|
|
||||||
) (bool, error) {
|
|
||||||
// For remote accounts, only show
|
// For remote accounts, only show
|
||||||
// Public statuses via the web.
|
// Public statuses via the web.
|
||||||
if status.Account.IsRemote() {
|
if status.Account.IsRemote() {
|
||||||
|
@ -275,8 +274,7 @@ func (f *Filter) isStatusVisibleUnauthed(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
webVisibility := status.Account.Settings.WebVisibility
|
switch webvis := status.Account.Settings.WebVisibility; webvis {
|
||||||
switch webVisibility {
|
|
||||||
|
|
||||||
// public_only: status must be Public.
|
// public_only: status must be Public.
|
||||||
case gtsmodel.VisibilityPublic:
|
case gtsmodel.VisibilityPublic:
|
||||||
|
@ -296,7 +294,7 @@ func (f *Filter) isStatusVisibleUnauthed(
|
||||||
default:
|
default:
|
||||||
return false, gtserror.Newf(
|
return false, gtserror.Newf(
|
||||||
"unrecognized web visibility for account %s: %s",
|
"unrecognized web visibility for account %s: %s",
|
||||||
status.Account.ID, webVisibility,
|
status.Account.ID, webvis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,9 +48,6 @@ var (
|
||||||
|
|
||||||
// ErrReservedAddr is returned if a dialed address resolves to an IP within a blocked or reserved net.
|
// ErrReservedAddr is returned if a dialed address resolves to an IP within a blocked or reserved net.
|
||||||
ErrReservedAddr = errors.New("dial within blocked / reserved IP range")
|
ErrReservedAddr = errors.New("dial within blocked / reserved IP range")
|
||||||
|
|
||||||
// ErrBodyTooLarge is returned when a received response body is above predefined limit (default 40MB).
|
|
||||||
ErrBodyTooLarge = errors.New("body size too large")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config provides configuration details for setting up a new
|
// Config provides configuration details for setting up a new
|
||||||
|
@ -302,7 +299,6 @@ func (c *Client) do(r *Request) (rsp *http.Response, retry bool, err error) {
|
||||||
if errorsv2.IsV2(err,
|
if errorsv2.IsV2(err,
|
||||||
context.DeadlineExceeded,
|
context.DeadlineExceeded,
|
||||||
context.Canceled,
|
context.Canceled,
|
||||||
ErrBodyTooLarge,
|
|
||||||
ErrReservedAddr,
|
ErrReservedAddr,
|
||||||
) {
|
) {
|
||||||
// Non-retryable errors.
|
// Non-retryable errors.
|
||||||
|
|
|
@ -224,8 +224,11 @@ func ffprobe(ctx context.Context, filepath string) (*result, error) {
|
||||||
// Show specifically stream codec names, types, frame rate, duration, dimens, and pixel format.
|
// Show specifically stream codec names, types, frame rate, duration, dimens, and pixel format.
|
||||||
"stream=codec_name,codec_type,r_frame_rate,duration_ts,width,height,pix_fmt" + ":" +
|
"stream=codec_name,codec_type,r_frame_rate,duration_ts,width,height,pix_fmt" + ":" +
|
||||||
|
|
||||||
// Show orientation.
|
// Show orientation tag.
|
||||||
"tags=orientation",
|
"tags=orientation" + ":" +
|
||||||
|
|
||||||
|
// Show rotation data.
|
||||||
|
"side_data=rotation",
|
||||||
|
|
||||||
// Limit to reading the first
|
// Limit to reading the first
|
||||||
// 1s of data looking for "rotation"
|
// 1s of data looking for "rotation"
|
||||||
|
@ -490,7 +493,7 @@ func (res *ffprobeResult) Process() (*result, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check extra packet / frame information
|
// Check extra packet / frame information
|
||||||
// for provided orientation (not always set).
|
// for provided orientation (if provided).
|
||||||
for _, pf := range res.PacketsAndFrames {
|
for _, pf := range res.PacketsAndFrames {
|
||||||
|
|
||||||
// Ensure frame contains tags.
|
// Ensure frame contains tags.
|
||||||
|
@ -498,23 +501,24 @@ func (res *ffprobeResult) Process() (*result, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure orientation not
|
|
||||||
// already been specified.
|
|
||||||
if r.orientation != 0 {
|
|
||||||
return nil, errors.New("multiple sets of orientation data")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim any space from orientation value.
|
// Trim any space from orientation value.
|
||||||
str := strings.TrimSpace(pf.Tags.Orientation)
|
str := strings.TrimSpace(pf.Tags.Orientation)
|
||||||
|
|
||||||
// Parse as integer value.
|
// Parse as integer value.
|
||||||
i, _ := strconv.Atoi(str)
|
orient, _ := strconv.Atoi(str)
|
||||||
if i < 0 || i >= 9 {
|
if orient < 0 || orient >= 9 {
|
||||||
return nil, errors.New("invalid orientation data")
|
return nil, errors.New("invalid orientation data")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set orientation.
|
// Ensure different value has
|
||||||
r.orientation = i
|
// not already been specified.
|
||||||
|
if r.orientation != 0 &&
|
||||||
|
orient != r.orientation {
|
||||||
|
return nil, errors.New("multiple sets of orientation / rotation data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new orientation.
|
||||||
|
r.orientation = orient
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preallocate streams to max possible lengths.
|
// Preallocate streams to max possible lengths.
|
||||||
|
@ -554,6 +558,57 @@ func (res *ffprobeResult) Process() (*result, error) {
|
||||||
framerate = float32(num / den)
|
framerate = float32(num / den)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for embedded sidedata
|
||||||
|
// which may contain rotation data.
|
||||||
|
for _, d := range s.SideDataList {
|
||||||
|
|
||||||
|
// Ensure frame side
|
||||||
|
// data IS rotation data.
|
||||||
|
if d.Rotation == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop any decimal
|
||||||
|
// rotation value.
|
||||||
|
rot := int(d.Rotation)
|
||||||
|
|
||||||
|
// Round rotation to multiple of 90.
|
||||||
|
// More granularity is not needed.
|
||||||
|
if q := rot % 90; q > 45 {
|
||||||
|
rot += (90 - q)
|
||||||
|
} else {
|
||||||
|
rot -= q
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop any value above 360
|
||||||
|
// or below -360, these are
|
||||||
|
// just repeat full turns.
|
||||||
|
//
|
||||||
|
// Then convert to
|
||||||
|
// orientation value.
|
||||||
|
var orient int
|
||||||
|
switch rot % 360 {
|
||||||
|
case 0:
|
||||||
|
orient = orientationNormal
|
||||||
|
case 90, -270:
|
||||||
|
orient = orientationRotate90
|
||||||
|
case 180:
|
||||||
|
orient = orientationRotate180
|
||||||
|
case 270, -90:
|
||||||
|
orient = orientationRotate270
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure different value has
|
||||||
|
// not already been specified.
|
||||||
|
if r.orientation != 0 &&
|
||||||
|
orient != r.orientation {
|
||||||
|
return nil, errors.New("multiple sets of orientation / rotation data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new orientation.
|
||||||
|
r.orientation = orient
|
||||||
|
}
|
||||||
|
|
||||||
// Append video stream data to result.
|
// Append video stream data to result.
|
||||||
r.video = append(r.video, videoStream{
|
r.video = append(r.video, videoStream{
|
||||||
stream: stream{codec: s.CodecName},
|
stream: stream{codec: s.CodecName},
|
||||||
|
@ -580,6 +635,7 @@ type ffprobeResult struct {
|
||||||
type ffprobePacketOrFrame struct {
|
type ffprobePacketOrFrame struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Tags ffprobeTags `json:"tags"`
|
Tags ffprobeTags `json:"tags"`
|
||||||
|
// SideDataList []ffprobeSideData `json:"side_data_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ffprobeTags struct {
|
type ffprobeTags struct {
|
||||||
|
@ -587,13 +643,18 @@ type ffprobeTags struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ffprobeStream struct {
|
type ffprobeStream struct {
|
||||||
CodecName string `json:"codec_name"`
|
CodecName string `json:"codec_name"`
|
||||||
CodecType string `json:"codec_type"`
|
CodecType string `json:"codec_type"`
|
||||||
PixFmt string `json:"pix_fmt"`
|
PixFmt string `json:"pix_fmt"`
|
||||||
RFrameRate string `json:"r_frame_rate"`
|
RFrameRate string `json:"r_frame_rate"`
|
||||||
DurationTS uint `json:"duration_ts"`
|
DurationTS uint `json:"duration_ts"`
|
||||||
Width int `json:"width"`
|
Width int `json:"width"`
|
||||||
Height int `json:"height"`
|
Height int `json:"height"`
|
||||||
|
SideDataList []ffprobeSideData `json:"side_data_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ffprobeSideData struct {
|
||||||
|
Rotation float64 `json:"rotation"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ffprobeFormat struct {
|
type ffprobeFormat struct {
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"errors"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
@ -29,25 +29,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
allowMatches = matchstats{m: make(map[string]uint64)}
|
// errors set on gin context by header filter middleware.
|
||||||
blockMatches = matchstats{m: make(map[string]uint64)}
|
errHeaderNotAllowed = errors.New("header did not match allow filter")
|
||||||
|
errHeaderBlocked = errors.New("header matched block filter")
|
||||||
)
|
)
|
||||||
|
|
||||||
// matchstats is a simple statistics
|
|
||||||
// counter for header filter matches.
|
|
||||||
// TODO: replace with otel.
|
|
||||||
type matchstats struct {
|
|
||||||
m map[string]uint64
|
|
||||||
l sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *matchstats) Add(hdr, regex string) {
|
|
||||||
m.l.Lock()
|
|
||||||
key := hdr + ":" + regex
|
|
||||||
m.m[key]++
|
|
||||||
m.l.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HeaderFilter returns a gin middleware handler that provides HTTP
|
// HeaderFilter returns a gin middleware handler that provides HTTP
|
||||||
// request blocking (filtering) based on database allow / block filters.
|
// request blocking (filtering) based on database allow / block filters.
|
||||||
func HeaderFilter(state *state.State) gin.HandlerFunc {
|
func HeaderFilter(state *state.State) gin.HandlerFunc {
|
||||||
|
@ -83,6 +69,7 @@ func headerFilterAllowMode(state *state.State) func(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if block {
|
if block {
|
||||||
|
_ = c.Error(errHeaderBlocked)
|
||||||
respondBlocked(c)
|
respondBlocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -95,6 +82,7 @@ func headerFilterAllowMode(state *state.State) func(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if notAllow {
|
if notAllow {
|
||||||
|
_ = c.Error(errHeaderNotAllowed)
|
||||||
respondBlocked(c)
|
respondBlocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -129,6 +117,7 @@ func headerFilterBlockMode(state *state.State) func(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if block {
|
if block {
|
||||||
|
_ = c.Error(errHeaderBlocked)
|
||||||
respondBlocked(c)
|
respondBlocked(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -146,7 +135,7 @@ func isHeaderBlocked(state *state.State, c *gin.Context) (bool, error) {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Perform an explicit is-blocked check on request header.
|
// Perform an explicit is-blocked check on request header.
|
||||||
key, expr, err := state.DB.BlockHeaderRegularMatch(ctx, hdr)
|
key, _, err := state.DB.BlockHeaderRegularMatch(ctx, hdr)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
break
|
break
|
||||||
|
@ -161,12 +150,10 @@ func isHeaderBlocked(state *state.State, c *gin.Context) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if key != "" {
|
if key != "" {
|
||||||
if expr != "" {
|
// if expr != "" {
|
||||||
// Increment block matches stat.
|
// // TODO: replace expvar with build
|
||||||
// TODO: replace expvar with build
|
// // taggable metrics types in State{}.
|
||||||
// taggable metrics types in State{}.
|
// }
|
||||||
blockMatches.Add(key, expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A header was matched against!
|
// A header was matched against!
|
||||||
// i.e. this request is blocked.
|
// i.e. this request is blocked.
|
||||||
|
@ -183,7 +170,7 @@ func isHeaderAllowed(state *state.State, c *gin.Context) (bool, error) {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Perform an explicit is-allowed check on request header.
|
// Perform an explicit is-allowed check on request header.
|
||||||
key, expr, err := state.DB.AllowHeaderRegularMatch(ctx, hdr)
|
key, _, err := state.DB.AllowHeaderRegularMatch(ctx, hdr)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
break
|
break
|
||||||
|
@ -198,12 +185,10 @@ func isHeaderAllowed(state *state.State, c *gin.Context) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if key != "" {
|
if key != "" {
|
||||||
if expr != "" {
|
// if expr != "" {
|
||||||
// Increment allow matches stat.
|
// // TODO: replace expvar with build
|
||||||
// TODO: replace expvar with build
|
// // taggable metrics types in State{}.
|
||||||
// taggable metrics types in State{}.
|
// }
|
||||||
allowMatches.Add(key, expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A header was matched against!
|
// A header was matched against!
|
||||||
// i.e. this request is allowed.
|
// i.e. this request is allowed.
|
||||||
|
@ -220,7 +205,7 @@ func isHeaderNotAllowed(state *state.State, c *gin.Context) (bool, error) {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Perform an explicit is-NOT-allowed check on request header.
|
// Perform an explicit is-NOT-allowed check on request header.
|
||||||
key, expr, err := state.DB.AllowHeaderInverseMatch(ctx, hdr)
|
key, _, err := state.DB.AllowHeaderInverseMatch(ctx, hdr)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
break
|
break
|
||||||
|
@ -235,12 +220,10 @@ func isHeaderNotAllowed(state *state.State, c *gin.Context) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if key != "" {
|
if key != "" {
|
||||||
if expr != "" {
|
// if expr != "" {
|
||||||
// Increment allow matches stat.
|
// // TODO: replace expvar with build
|
||||||
// TODO: replace expvar with build
|
// // taggable metrics types in State{}.
|
||||||
// taggable metrics types in State{}.
|
// }
|
||||||
allowMatches.Add(key, expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A header was matched against!
|
// A header was matched against!
|
||||||
// i.e. request is NOT allowed.
|
// i.e. request is NOT allowed.
|
||||||
|
|
|
@ -42,6 +42,7 @@ func (p *Processor) GetTargetAccountBy(
|
||||||
// Fetch the target account from db.
|
// Fetch the target account from db.
|
||||||
target, err := getTargetFromDB()
|
target, err := getTargetFromDB()
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err := gtserror.Newf("error getting from db: %w", err)
|
||||||
return nil, false, gtserror.NewErrorInternalError(err)
|
return nil, false, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +58,7 @@ func (p *Processor) GetTargetAccountBy(
|
||||||
// Check whether target account is visible to requesting account.
|
// Check whether target account is visible to requesting account.
|
||||||
visible, err = p.visFilter.AccountVisible(ctx, requester, target)
|
visible, err = p.visFilter.AccountVisible(ctx, requester, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error checking visibility: %w", err)
|
||||||
return nil, false, gtserror.NewErrorInternalError(err)
|
return nil, false, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +130,8 @@ func (p *Processor) GetVisibleTargetAccount(
|
||||||
return target, nil
|
return target, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAPIAccount fetches the appropriate API account model depending on whether requester = target.
|
// GetAPIAccount fetches the appropriate API account
|
||||||
|
// model depending on whether requester = target.
|
||||||
func (p *Processor) GetAPIAccount(
|
func (p *Processor) GetAPIAccount(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
requester *gtsmodel.Account,
|
requester *gtsmodel.Account,
|
||||||
|
@ -148,14 +151,15 @@ func (p *Processor) GetAPIAccount(
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := gtserror.Newf("error converting account: %w", err)
|
err := gtserror.Newf("error converting: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiAcc, nil
|
return apiAcc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAPIAccountBlocked fetches the limited "blocked" account model for given target.
|
// GetAPIAccountBlocked fetches the limited
|
||||||
|
// "blocked" account model for given target.
|
||||||
func (p *Processor) GetAPIAccountBlocked(
|
func (p *Processor) GetAPIAccountBlocked(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
targetAcc *gtsmodel.Account,
|
targetAcc *gtsmodel.Account,
|
||||||
|
@ -165,7 +169,7 @@ func (p *Processor) GetAPIAccountBlocked(
|
||||||
) {
|
) {
|
||||||
apiAccount, err := p.converter.AccountToAPIAccountBlocked(ctx, targetAcc)
|
apiAccount, err := p.converter.AccountToAPIAccountBlocked(ctx, targetAcc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gtserror.Newf("error converting account: %w", err)
|
err := gtserror.Newf("error converting: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
return apiAccount, nil
|
return apiAccount, nil
|
||||||
|
@ -182,7 +186,7 @@ func (p *Processor) GetAPIAccountSensitive(
|
||||||
) {
|
) {
|
||||||
apiAccount, err := p.converter.AccountToAPIAccountSensitive(ctx, targetAcc)
|
apiAccount, err := p.converter.AccountToAPIAccountSensitive(ctx, targetAcc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gtserror.Newf("error converting account: %w", err)
|
err := gtserror.Newf("error converting: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
return apiAccount, nil
|
return apiAccount, nil
|
||||||
|
@ -226,8 +230,7 @@ func (p *Processor) getVisibleAPIAccounts(
|
||||||
) []*apimodel.Account {
|
) []*apimodel.Account {
|
||||||
// Start new log entry with
|
// Start new log entry with
|
||||||
// the above calling func's name.
|
// the above calling func's name.
|
||||||
l := log.
|
l := log.WithContext(ctx).
|
||||||
WithContext(ctx).
|
|
||||||
WithField("caller", log.Caller(calldepth+1))
|
WithField("caller", log.Caller(calldepth+1))
|
||||||
|
|
||||||
// Preallocate slice according to expected length.
|
// Preallocate slice according to expected length.
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
||||||
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -50,6 +51,7 @@ func (p *Processor) GetTargetStatusBy(
|
||||||
// Fetch the target status from db.
|
// Fetch the target status from db.
|
||||||
target, err := getTargetFromDB()
|
target, err := getTargetFromDB()
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err := gtserror.Newf("error getting from db: %w", err)
|
||||||
return nil, false, gtserror.NewErrorInternalError(err)
|
return nil, false, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +67,7 @@ func (p *Processor) GetTargetStatusBy(
|
||||||
// Check whether target status is visible to requesting account.
|
// Check whether target status is visible to requesting account.
|
||||||
visible, err = p.visFilter.StatusVisible(ctx, requester, target)
|
visible, err = p.visFilter.StatusVisible(ctx, requester, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error checking visibility: %w", err)
|
||||||
return nil, false, gtserror.NewErrorInternalError(err)
|
return nil, false, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,14 +177,83 @@ func (p *Processor) GetAPIStatus(
|
||||||
apiStatus *apimodel.Status,
|
apiStatus *apimodel.Status,
|
||||||
errWithCode gtserror.WithCode,
|
errWithCode gtserror.WithCode,
|
||||||
) {
|
) {
|
||||||
apiStatus, err := p.converter.StatusToAPIStatus(ctx, target, requester, statusfilter.FilterContextNone, nil, nil)
|
apiStatus, err := p.converter.StatusToAPIStatus(ctx,
|
||||||
|
target,
|
||||||
|
requester,
|
||||||
|
statusfilter.FilterContextNone,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gtserror.Newf("error converting status: %w", err)
|
err := gtserror.Newf("error converting: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
return apiStatus, nil
|
return apiStatus, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetVisibleAPIStatuses converts a slice of statuses to API
|
||||||
|
// model statuses, filtering according to visibility to requester
|
||||||
|
// along with given filter context, filters and user mutes.
|
||||||
|
//
|
||||||
|
// Please note that all errors will be logged at ERROR level,
|
||||||
|
// but will not be returned. Callers are likely to run into
|
||||||
|
// show-stopping errors in the lead-up to this function.
|
||||||
|
func (p *Processor) GetVisibleAPIStatuses(
|
||||||
|
ctx context.Context,
|
||||||
|
requester *gtsmodel.Account,
|
||||||
|
statuses []*gtsmodel.Status,
|
||||||
|
filterContext statusfilter.FilterContext,
|
||||||
|
filters []*gtsmodel.Filter,
|
||||||
|
userMutes []*gtsmodel.UserMute,
|
||||||
|
) []apimodel.Status {
|
||||||
|
|
||||||
|
// Start new log entry with
|
||||||
|
// the calling function name
|
||||||
|
// as a field in each entry.
|
||||||
|
l := log.WithContext(ctx).
|
||||||
|
WithField("caller", log.Caller(3))
|
||||||
|
|
||||||
|
// Compile mutes to useable user mutes for type converter.
|
||||||
|
compUserMutes := usermute.NewCompiledUserMuteList(userMutes)
|
||||||
|
|
||||||
|
// Iterate filtered statuses for conversion to API model.
|
||||||
|
apiStatuses := make([]apimodel.Status, 0, len(statuses))
|
||||||
|
for _, status := range statuses {
|
||||||
|
|
||||||
|
// Check whether status is visible to requester.
|
||||||
|
visible, err := p.visFilter.StatusVisible(ctx,
|
||||||
|
requester,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
l.Errorf("error checking visibility: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visible {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to API status, taking mute / filter into account.
|
||||||
|
apiStatus, err := p.converter.StatusToAPIStatus(ctx,
|
||||||
|
status,
|
||||||
|
requester,
|
||||||
|
filterContext,
|
||||||
|
filters,
|
||||||
|
compUserMutes,
|
||||||
|
)
|
||||||
|
if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) {
|
||||||
|
l.Errorf("error converting: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append converted status to return slice.
|
||||||
|
apiStatuses = append(apiStatuses, *apiStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiStatuses
|
||||||
|
}
|
||||||
|
|
||||||
// InvalidateTimelinedStatus is a shortcut function for invalidating the cached
|
// InvalidateTimelinedStatus is a shortcut function for invalidating the cached
|
||||||
// representation one status in the home timeline and all list timelines of the
|
// representation one status in the home timeline and all list timelines of the
|
||||||
// given accountID. It should only be called in cases where a status update
|
// given accountID. It should only be called in cases where a status update
|
||||||
|
|
|
@ -223,7 +223,7 @@ func NewProcessor(
|
||||||
processor.tags = tags.New(state, converter)
|
processor.tags = tags.New(state, converter)
|
||||||
processor.timeline = timeline.New(state, converter, visFilter)
|
processor.timeline = timeline.New(state, converter, visFilter)
|
||||||
processor.search = search.New(state, federator, converter, visFilter)
|
processor.search = search.New(state, federator, converter, visFilter)
|
||||||
processor.status = status.New(state, &common, &processor.polls, federator, converter, visFilter, intFilter, parseMentionFunc)
|
processor.status = status.New(state, &common, &processor.polls, &processor.interactionRequests, federator, converter, visFilter, intFilter, parseMentionFunc)
|
||||||
processor.user = user.New(state, converter, oauthServer, emailSender)
|
processor.user = user.New(state, converter, oauthServer, emailSender)
|
||||||
|
|
||||||
// The advanced migrations processor sequences advanced migrations from all other processors.
|
// The advanced migrations processor sequences advanced migrations from all other processors.
|
||||||
|
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BoostCreate processes the boost/reblog of target
|
// BoostCreate processes the boost/reblog of target
|
||||||
|
@ -138,6 +139,23 @@ func (p *Processor) BoostCreate(
|
||||||
Target: target.Account,
|
Target: target.Account,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If the boost target status replies to a status
|
||||||
|
// that we own, and has a pending interaction
|
||||||
|
// request, use the boost as an implicit accept.
|
||||||
|
implicitlyAccepted, errWithCode := p.implicitlyAccept(ctx,
|
||||||
|
requester, target,
|
||||||
|
)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we ended up implicitly accepting, mark the
|
||||||
|
// target status as no longer pending approval so
|
||||||
|
// it's serialized properly via the API.
|
||||||
|
if implicitlyAccepted {
|
||||||
|
target.PendingApproval = util.Ptr(false)
|
||||||
|
}
|
||||||
|
|
||||||
return p.c.GetAPIStatus(ctx, requester, boost)
|
return p.c.GetAPIStatus(ctx, requester, boost)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ import (
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
@ -308,22 +307,7 @@ func (p *Processor) ContextGet(
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
convert := func(
|
// Retrieve the full thread context.
|
||||||
ctx context.Context,
|
|
||||||
status *gtsmodel.Status,
|
|
||||||
requestingAccount *gtsmodel.Account,
|
|
||||||
) (*apimodel.Status, error) {
|
|
||||||
return p.converter.StatusToAPIStatus(
|
|
||||||
ctx,
|
|
||||||
status,
|
|
||||||
requestingAccount,
|
|
||||||
statusfilter.FilterContextThread,
|
|
||||||
filters,
|
|
||||||
usermute.NewCompiledUserMuteList(mutes),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the thread context.
|
|
||||||
threadContext, errWithCode := p.contextGet(
|
threadContext, errWithCode := p.contextGet(
|
||||||
ctx,
|
ctx,
|
||||||
requester,
|
requester,
|
||||||
|
@ -333,34 +317,27 @@ func (p *Processor) ContextGet(
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
apiContext := &apimodel.ThreadContext{
|
var apiContext apimodel.ThreadContext
|
||||||
Ancestors: make([]apimodel.Status, 0, len(threadContext.ancestors)),
|
|
||||||
Descendants: make([]apimodel.Status, 0, len(threadContext.descendants)),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert ancestors + filter
|
// Convert and filter the thread context ancestors.
|
||||||
// out ones that aren't visible.
|
apiContext.Ancestors = p.c.GetVisibleAPIStatuses(ctx,
|
||||||
for _, status := range threadContext.ancestors {
|
requester,
|
||||||
if v, err := p.visFilter.StatusVisible(ctx, requester, status); err == nil && v {
|
threadContext.ancestors,
|
||||||
status, err := convert(ctx, status, requester)
|
statusfilter.FilterContextThread,
|
||||||
if err == nil {
|
filters,
|
||||||
apiContext.Ancestors = append(apiContext.Ancestors, *status)
|
mutes,
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert descendants + filter
|
// Convert and filter the thread context descendants
|
||||||
// out ones that aren't visible.
|
apiContext.Descendants = p.c.GetVisibleAPIStatuses(ctx,
|
||||||
for _, status := range threadContext.descendants {
|
requester,
|
||||||
if v, err := p.visFilter.StatusVisible(ctx, requester, status); err == nil && v {
|
threadContext.descendants,
|
||||||
status, err := convert(ctx, status, requester)
|
statusfilter.FilterContextThread,
|
||||||
if err == nil {
|
filters,
|
||||||
apiContext.Descendants = append(apiContext.Descendants, *status)
|
mutes,
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiContext, nil
|
return &apiContext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebContextGet is like ContextGet, but is explicitly
|
// WebContextGet is like ContextGet, but is explicitly
|
||||||
|
|
|
@ -164,6 +164,23 @@ func (p *Processor) Create(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the new status replies to a status that
|
||||||
|
// replies to us, use our reply as an implicit
|
||||||
|
// accept of any pending interaction.
|
||||||
|
implicitlyAccepted, errWithCode := p.implicitlyAccept(ctx,
|
||||||
|
requester, status,
|
||||||
|
)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we ended up implicitly accepting, mark the
|
||||||
|
// replied-to status as no longer pending approval
|
||||||
|
// so it's serialized properly via the API.
|
||||||
|
if implicitlyAccepted {
|
||||||
|
status.InReplyTo.PendingApproval = util.Ptr(false)
|
||||||
|
}
|
||||||
|
|
||||||
return p.c.GetAPIStatus(ctx, requester, status)
|
return p.c.GetAPIStatus(ctx, requester, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Processor) getFaveableStatus(
|
func (p *Processor) getFaveableStatus(
|
||||||
|
@ -138,8 +139,6 @@ func (p *Processor) FaveCreate(
|
||||||
pendingApproval = false
|
pendingApproval = false
|
||||||
}
|
}
|
||||||
|
|
||||||
status.PendingApproval = &pendingApproval
|
|
||||||
|
|
||||||
// Create a new fave, marking it
|
// Create a new fave, marking it
|
||||||
// as pending approval if necessary.
|
// as pending approval if necessary.
|
||||||
faveID := id.NewULID()
|
faveID := id.NewULID()
|
||||||
|
@ -157,7 +156,7 @@ func (p *Processor) FaveCreate(
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.state.DB.PutStatusFave(ctx, gtsFave); err != nil {
|
if err := p.state.DB.PutStatusFave(ctx, gtsFave); err != nil {
|
||||||
err = fmt.Errorf("FaveCreate: error putting fave in database: %w", err)
|
err = gtserror.Newf("db error putting fave: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,6 +169,23 @@ func (p *Processor) FaveCreate(
|
||||||
Target: status.Account,
|
Target: status.Account,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If the fave target status replies to a status
|
||||||
|
// that we own, and has a pending interaction
|
||||||
|
// request, use the fave as an implicit accept.
|
||||||
|
implicitlyAccepted, errWithCode := p.implicitlyAccept(ctx,
|
||||||
|
requester, status,
|
||||||
|
)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we ended up implicitly accepting, mark the
|
||||||
|
// target status as no longer pending approval so
|
||||||
|
// it's serialized properly via the API.
|
||||||
|
if implicitlyAccepted {
|
||||||
|
status.PendingApproval = util.Ptr(false)
|
||||||
|
}
|
||||||
|
|
||||||
return p.c.GetAPIStatus(ctx, requester, status)
|
return p.c.GetAPIStatus(ctx, requester, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing/interactionrequests"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/polls"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/polls"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||||
|
@ -42,7 +43,8 @@ type Processor struct {
|
||||||
parseMention gtsmodel.ParseMentionFunc
|
parseMention gtsmodel.ParseMentionFunc
|
||||||
|
|
||||||
// other processors
|
// other processors
|
||||||
polls *polls.Processor
|
polls *polls.Processor
|
||||||
|
intReqs *interactionrequests.Processor
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new status processor.
|
// New returns a new status processor.
|
||||||
|
@ -50,6 +52,7 @@ func New(
|
||||||
state *state.State,
|
state *state.State,
|
||||||
common *common.Processor,
|
common *common.Processor,
|
||||||
polls *polls.Processor,
|
polls *polls.Processor,
|
||||||
|
intReqs *interactionrequests.Processor,
|
||||||
federator *federation.Federator,
|
federator *federation.Federator,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
visFilter *visibility.Filter,
|
visFilter *visibility.Filter,
|
||||||
|
@ -66,5 +69,6 @@ func New(
|
||||||
formatter: text.NewFormatter(state.DB),
|
formatter: text.NewFormatter(state.DB),
|
||||||
parseMention: parseMention,
|
parseMention: parseMention,
|
||||||
polls: polls,
|
polls: polls,
|
||||||
|
intReqs: intReqs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing/interactionrequests"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/polls"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/polls"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
@ -100,11 +101,13 @@ func (suite *StatusStandardTestSuite) SetupTest() {
|
||||||
|
|
||||||
common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, visFilter)
|
common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, visFilter)
|
||||||
polls := polls.New(&common, &suite.state, suite.typeConverter)
|
polls := polls.New(&common, &suite.state, suite.typeConverter)
|
||||||
|
intReqs := interactionrequests.New(&common, &suite.state, suite.typeConverter)
|
||||||
|
|
||||||
suite.status = status.New(
|
suite.status = status.New(
|
||||||
&suite.state,
|
&suite.state,
|
||||||
&common,
|
&common,
|
||||||
&polls,
|
&polls,
|
||||||
|
&intReqs,
|
||||||
suite.federator,
|
suite.federator,
|
||||||
suite.typeConverter,
|
suite.typeConverter,
|
||||||
visFilter,
|
visFilter,
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Processor) implicitlyAccept(
|
||||||
|
ctx context.Context,
|
||||||
|
requester *gtsmodel.Account,
|
||||||
|
status *gtsmodel.Status,
|
||||||
|
) (bool, gtserror.WithCode) {
|
||||||
|
if status.InReplyToAccountID != requester.ID {
|
||||||
|
// Status doesn't reply to us,
|
||||||
|
// we can't accept on behalf
|
||||||
|
// of someone else.
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPendingApproval := util.PtrOrValue(status.PendingApproval, false)
|
||||||
|
if !targetPendingApproval {
|
||||||
|
// Status isn't pending approval,
|
||||||
|
// nothing to implicitly accept.
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status is pending approval,
|
||||||
|
// check for an interaction request.
|
||||||
|
intReq, err := p.state.DB.GetInteractionRequestByInteractionURI(ctx, status.URI)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
// Something's gone wrong.
|
||||||
|
err := gtserror.Newf("db error getting interaction request for %s: %w", status.URI, err)
|
||||||
|
return false, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No interaction request present
|
||||||
|
// for this status. Race condition?
|
||||||
|
if intReq == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept the interaction.
|
||||||
|
if _, errWithCode := p.intReqs.Accept(ctx,
|
||||||
|
requester, intReq.ID,
|
||||||
|
); errWithCode != nil {
|
||||||
|
return false, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
|
@ -384,8 +384,9 @@ func (s *Surface) timelineStatus(
|
||||||
) (bool, error) {
|
) (bool, error) {
|
||||||
|
|
||||||
// Ingest status into given timeline using provided function.
|
// Ingest status into given timeline using provided function.
|
||||||
if inserted, err := ingest(ctx, timelineID, status); err != nil {
|
if inserted, err := ingest(ctx, timelineID, status); err != nil &&
|
||||||
err = gtserror.Newf("error ingesting status %s: %w", status.ID, err)
|
!errors.Is(err, statusfilter.ErrHideStatus) {
|
||||||
|
err := gtserror.Newf("error ingesting status %s: %w", status.ID, err)
|
||||||
return false, err
|
return false, err
|
||||||
} else if !inserted {
|
} else if !inserted {
|
||||||
// Nothing more to do.
|
// Nothing more to do.
|
||||||
|
@ -400,15 +401,19 @@ func (s *Surface) timelineStatus(
|
||||||
filters,
|
filters,
|
||||||
mutes,
|
mutes,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) {
|
||||||
err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
|
err := gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// The status was inserted so stream it to the user.
|
if apiStatus != nil {
|
||||||
s.Stream.Update(ctx, account, apiStatus, streamType)
|
// The status was inserted so stream it to the user.
|
||||||
|
s.Stream.Update(ctx, account, apiStatus, streamType)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
return true, nil
|
// Status was hidden.
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// timelineAndNotifyStatusForTagFollowers inserts the status into the
|
// timelineAndNotifyStatusForTagFollowers inserts the status into the
|
||||||
|
|
|
@ -473,19 +473,20 @@ const (
|
||||||
|
|
||||||
type TypeUtilsTestSuite struct {
|
type TypeUtilsTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
db db.DB
|
db db.DB
|
||||||
state state.State
|
state state.State
|
||||||
testAccounts map[string]*gtsmodel.Account
|
testAccounts map[string]*gtsmodel.Account
|
||||||
testStatuses map[string]*gtsmodel.Status
|
testStatuses map[string]*gtsmodel.Status
|
||||||
testAttachments map[string]*gtsmodel.MediaAttachment
|
testAttachments map[string]*gtsmodel.MediaAttachment
|
||||||
testPeople map[string]vocab.ActivityStreamsPerson
|
testPeople map[string]vocab.ActivityStreamsPerson
|
||||||
testEmojis map[string]*gtsmodel.Emoji
|
testEmojis map[string]*gtsmodel.Emoji
|
||||||
testReports map[string]*gtsmodel.Report
|
testReports map[string]*gtsmodel.Report
|
||||||
testMentions map[string]*gtsmodel.Mention
|
testMentions map[string]*gtsmodel.Mention
|
||||||
testPollVotes map[string]*gtsmodel.PollVote
|
testPollVotes map[string]*gtsmodel.PollVote
|
||||||
testFilters map[string]*gtsmodel.Filter
|
testFilters map[string]*gtsmodel.Filter
|
||||||
testFilterKeywords map[string]*gtsmodel.FilterKeyword
|
testFilterKeywords map[string]*gtsmodel.FilterKeyword
|
||||||
testFilterStatues map[string]*gtsmodel.FilterStatus
|
testFilterStatues map[string]*gtsmodel.FilterStatus
|
||||||
|
testInteractionRequests map[string]*gtsmodel.InteractionRequest
|
||||||
|
|
||||||
typeconverter *typeutils.Converter
|
typeconverter *typeutils.Converter
|
||||||
}
|
}
|
||||||
|
@ -512,6 +513,7 @@ func (suite *TypeUtilsTestSuite) SetupTest() {
|
||||||
suite.testFilters = testrig.NewTestFilters()
|
suite.testFilters = testrig.NewTestFilters()
|
||||||
suite.testFilterKeywords = testrig.NewTestFilterKeywords()
|
suite.testFilterKeywords = testrig.NewTestFilterKeywords()
|
||||||
suite.testFilterStatues = testrig.NewTestFilterStatuses()
|
suite.testFilterStatues = testrig.NewTestFilterStatuses()
|
||||||
|
suite.testInteractionRequests = testrig.NewTestInteractionRequests()
|
||||||
suite.typeconverter = typeutils.NewConverter(&suite.state)
|
suite.typeconverter = typeutils.NewConverter(&suite.state)
|
||||||
|
|
||||||
testrig.StandardDBSetup(suite.db, nil)
|
testrig.StandardDBSetup(suite.db, nil)
|
||||||
|
|
|
@ -803,26 +803,55 @@ func (c *Converter) TagToAPITag(ctx context.Context, t *gtsmodel.Tag, stubHistor
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusToAPIStatus converts a gts model status into its api
|
// StatusToAPIStatus converts a gts model
|
||||||
// (frontend) representation for serialization on the API.
|
// status into its api (frontend) representation
|
||||||
|
// for serialization on the API.
|
||||||
//
|
//
|
||||||
// Requesting account can be nil.
|
// Requesting account can be nil.
|
||||||
//
|
//
|
||||||
// Filter context can be the empty string if these statuses are not being filtered.
|
// filterContext can be the empty string
|
||||||
|
// if these statuses are not being filtered.
|
||||||
//
|
//
|
||||||
// If there is a matching "hide" filter, the returned status will be nil with a ErrHideStatus error;
|
// If there is a matching "hide" filter, the returned
|
||||||
// callers need to handle that case by excluding it from results.
|
// status will be nil with a ErrHideStatus error; callers
|
||||||
|
// need to handle that case by excluding it from results.
|
||||||
func (c *Converter) StatusToAPIStatus(
|
func (c *Converter) StatusToAPIStatus(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
s *gtsmodel.Status,
|
status *gtsmodel.Status,
|
||||||
requestingAccount *gtsmodel.Account,
|
requestingAccount *gtsmodel.Account,
|
||||||
filterContext statusfilter.FilterContext,
|
filterContext statusfilter.FilterContext,
|
||||||
filters []*gtsmodel.Filter,
|
filters []*gtsmodel.Filter,
|
||||||
mutes *usermute.CompiledUserMuteList,
|
mutes *usermute.CompiledUserMuteList,
|
||||||
|
) (*apimodel.Status, error) {
|
||||||
|
return c.statusToAPIStatus(
|
||||||
|
ctx,
|
||||||
|
status,
|
||||||
|
requestingAccount,
|
||||||
|
filterContext,
|
||||||
|
filters,
|
||||||
|
mutes,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// statusToAPIStatus is the package-internal implementation
|
||||||
|
// of StatusToAPIStatus that lets the caller customize whether
|
||||||
|
// to placehold unknown attachment types, and/or add a note
|
||||||
|
// about the status being pending and requiring approval.
|
||||||
|
func (c *Converter) statusToAPIStatus(
|
||||||
|
ctx context.Context,
|
||||||
|
status *gtsmodel.Status,
|
||||||
|
requestingAccount *gtsmodel.Account,
|
||||||
|
filterContext statusfilter.FilterContext,
|
||||||
|
filters []*gtsmodel.Filter,
|
||||||
|
mutes *usermute.CompiledUserMuteList,
|
||||||
|
placeholdAttachments bool,
|
||||||
|
addPendingNote bool,
|
||||||
) (*apimodel.Status, error) {
|
) (*apimodel.Status, error) {
|
||||||
apiStatus, err := c.statusToFrontend(
|
apiStatus, err := c.statusToFrontend(
|
||||||
ctx,
|
ctx,
|
||||||
s,
|
status,
|
||||||
requestingAccount, // Can be nil.
|
requestingAccount, // Can be nil.
|
||||||
filterContext, // Can be empty.
|
filterContext, // Can be empty.
|
||||||
filters,
|
filters,
|
||||||
|
@ -833,7 +862,7 @@ func (c *Converter) StatusToAPIStatus(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert author to API model.
|
// Convert author to API model.
|
||||||
acct, err := c.AccountToAPIAccountPublic(ctx, s.Account)
|
acct, err := c.AccountToAPIAccountPublic(ctx, status.Account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("error converting status acct: %w", err)
|
return nil, gtserror.Newf("error converting status acct: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -842,23 +871,43 @@ func (c *Converter) StatusToAPIStatus(
|
||||||
// Convert author of boosted
|
// Convert author of boosted
|
||||||
// status (if set) to API model.
|
// status (if set) to API model.
|
||||||
if apiStatus.Reblog != nil {
|
if apiStatus.Reblog != nil {
|
||||||
boostAcct, err := c.AccountToAPIAccountPublic(ctx, s.BoostOfAccount)
|
boostAcct, err := c.AccountToAPIAccountPublic(ctx, status.BoostOfAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("error converting boost acct: %w", err)
|
return nil, gtserror.Newf("error converting boost acct: %w", err)
|
||||||
}
|
}
|
||||||
apiStatus.Reblog.Account = boostAcct
|
apiStatus.Reblog.Account = boostAcct
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize status for API by pruning
|
if placeholdAttachments {
|
||||||
// attachments that were not locally
|
// Normalize status for API by pruning attachments
|
||||||
// stored, replacing them with a helpful
|
// that were not able to be locally stored, and replacing
|
||||||
// message + links to remote.
|
// them with a helpful message + links to remote.
|
||||||
var aside string
|
var attachNote string
|
||||||
aside, apiStatus.MediaAttachments = placeholderAttachments(apiStatus.MediaAttachments)
|
attachNote, apiStatus.MediaAttachments = placeholderAttachments(apiStatus.MediaAttachments)
|
||||||
apiStatus.Content += aside
|
apiStatus.Content += attachNote
|
||||||
if apiStatus.Reblog != nil {
|
|
||||||
aside, apiStatus.Reblog.MediaAttachments = placeholderAttachments(apiStatus.Reblog.MediaAttachments)
|
// Do the same for the reblogged status.
|
||||||
apiStatus.Reblog.Content += aside
|
if apiStatus.Reblog != nil {
|
||||||
|
attachNote, apiStatus.Reblog.MediaAttachments = placeholderAttachments(apiStatus.Reblog.MediaAttachments)
|
||||||
|
apiStatus.Reblog.Content += attachNote
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if addPendingNote {
|
||||||
|
// If this status is pending approval and
|
||||||
|
// replies to the requester, add a note
|
||||||
|
// about how to approve or reject the reply.
|
||||||
|
pendingApproval := util.PtrOrValue(status.PendingApproval, false)
|
||||||
|
if pendingApproval &&
|
||||||
|
requestingAccount != nil &&
|
||||||
|
requestingAccount.ID == status.InReplyToAccountID {
|
||||||
|
pendingNote, err := c.pendingReplyNote(ctx, status)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.Newf("error deriving 'pending reply' note: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiStatus.Content += pendingNote
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiStatus, nil
|
return apiStatus, nil
|
||||||
|
@ -1991,7 +2040,20 @@ func (c *Converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, s := range r.Statuses {
|
for _, s := range r.Statuses {
|
||||||
status, err := c.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextNone, nil, nil)
|
status, err := c.statusToAPIStatus(
|
||||||
|
ctx,
|
||||||
|
s,
|
||||||
|
requestingAccount,
|
||||||
|
statusfilter.FilterContextNone,
|
||||||
|
nil, // No filters.
|
||||||
|
nil, // No mutes.
|
||||||
|
true, // Placehold unknown attachments.
|
||||||
|
|
||||||
|
// Don't add note about
|
||||||
|
// pending, it's not
|
||||||
|
// relevant here.
|
||||||
|
false,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err)
|
return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err)
|
||||||
}
|
}
|
||||||
|
@ -2628,8 +2690,8 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
|
||||||
req.Status,
|
req.Status,
|
||||||
requestingAcct,
|
requestingAcct,
|
||||||
statusfilter.FilterContextNone,
|
statusfilter.FilterContextNone,
|
||||||
nil,
|
nil, // No filters.
|
||||||
nil,
|
nil, // No mutes.
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := gtserror.Newf("error converting interacted status: %w", err)
|
err := gtserror.Newf("error converting interacted status: %w", err)
|
||||||
|
@ -2638,13 +2700,20 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
|
||||||
|
|
||||||
var reply *apimodel.Status
|
var reply *apimodel.Status
|
||||||
if req.InteractionType == gtsmodel.InteractionReply {
|
if req.InteractionType == gtsmodel.InteractionReply {
|
||||||
reply, err = c.StatusToAPIStatus(
|
reply, err = c.statusToAPIStatus(
|
||||||
ctx,
|
ctx,
|
||||||
req.Reply,
|
req.Reply,
|
||||||
requestingAcct,
|
requestingAcct,
|
||||||
statusfilter.FilterContextNone,
|
statusfilter.FilterContextNone,
|
||||||
nil,
|
nil, // No filters.
|
||||||
nil,
|
nil, // No mutes.
|
||||||
|
true, // Placehold unknown attachments.
|
||||||
|
|
||||||
|
// Don't add note about pending;
|
||||||
|
// requester already knows it's
|
||||||
|
// pending because they're looking
|
||||||
|
// at the request right now.
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := gtserror.Newf("error converting reply: %w", err)
|
err := gtserror.Newf("error converting reply: %w", err)
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package typeutils_test
|
package typeutils_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -1709,6 +1710,130 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteraction
|
||||||
}`, string(b))
|
}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *InternalToFrontendTestSuite) TestStatusToAPIStatusPendingApproval() {
|
||||||
|
var (
|
||||||
|
testStatus = suite.testStatuses["admin_account_status_5"]
|
||||||
|
requestingAccount = suite.testAccounts["local_account_2"]
|
||||||
|
)
|
||||||
|
|
||||||
|
apiStatus, err := suite.typeconverter.StatusToAPIStatus(
|
||||||
|
context.Background(),
|
||||||
|
testStatus,
|
||||||
|
requestingAccount,
|
||||||
|
statusfilter.FilterContextNone,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to see the HTML in
|
||||||
|
// the status so don't escape it.
|
||||||
|
out := new(bytes.Buffer)
|
||||||
|
enc := json.NewEncoder(out)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
if err := enc.Encode(apiStatus); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Equal(`{
|
||||||
|
"id": "01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||||
|
"created_at": "2024-02-20T10:41:37.000Z",
|
||||||
|
"in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||||
|
"in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"visibility": "unlisted",
|
||||||
|
"language": null,
|
||||||
|
"uri": "http://localhost:8080/users/admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||||
|
"url": "http://localhost:8080/@admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||||
|
"replies_count": 0,
|
||||||
|
"reblogs_count": 0,
|
||||||
|
"favourites_count": 0,
|
||||||
|
"favourited": false,
|
||||||
|
"reblogged": false,
|
||||||
|
"muted": false,
|
||||||
|
"bookmarked": false,
|
||||||
|
"pinned": false,
|
||||||
|
"content": "<p>Hi <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span>, can I reply?</p><hr><p><i lang=\"en\">ℹ️ Note from localhost:8080: This reply is pending your approval. You can quickly accept it by liking, boosting or replying to it. You can also accept or reject it at the following link: <a href=\"http://localhost:8080/settings/user/interaction_requests/01J5QVXCCEATJYSXM9H6MZT4JR\" rel=\"noreferrer noopener nofollow\" target=\"_blank\">http://localhost:8080/settings/user/interaction_requests/01J5QVXCCEATJYSXM9H6MZT4JR</a>.</i></p>",
|
||||||
|
"reblog": null,
|
||||||
|
"application": {
|
||||||
|
"name": "superseriousbusiness",
|
||||||
|
"website": "https://superserious.business"
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
|
"username": "admin",
|
||||||
|
"acct": "admin",
|
||||||
|
"display_name": "",
|
||||||
|
"locked": false,
|
||||||
|
"discoverable": true,
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2022-05-17T13:10:59.000Z",
|
||||||
|
"note": "",
|
||||||
|
"url": "http://localhost:8080/@admin",
|
||||||
|
"avatar": "",
|
||||||
|
"avatar_static": "",
|
||||||
|
"header": "http://localhost:8080/assets/default_header.webp",
|
||||||
|
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||||
|
"followers_count": 1,
|
||||||
|
"following_count": 1,
|
||||||
|
"statuses_count": 4,
|
||||||
|
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"enable_rss": true,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"id": "admin",
|
||||||
|
"name": "admin",
|
||||||
|
"color": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [
|
||||||
|
{
|
||||||
|
"id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
|
"username": "1happyturtle",
|
||||||
|
"url": "http://localhost:8080/@1happyturtle",
|
||||||
|
"acct": "1happyturtle"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [],
|
||||||
|
"emojis": [],
|
||||||
|
"card": null,
|
||||||
|
"poll": null,
|
||||||
|
"text": "Hi @1happyturtle, can I reply?",
|
||||||
|
"interaction_policy": {
|
||||||
|
"can_favourite": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reply": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reblog": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, out.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *InternalToFrontendTestSuite) TestVideoAttachmentToFrontend() {
|
func (suite *InternalToFrontendTestSuite) TestVideoAttachmentToFrontend() {
|
||||||
testAttachment := suite.testAttachments["local_account_1_status_4_attachment_2"]
|
testAttachment := suite.testAttachments["local_account_1_status_4_attachment_2"]
|
||||||
apiAttachment, err := suite.typeconverter.AttachmentToAPIAttachment(context.Background(), testAttachment)
|
apiAttachment, err := suite.typeconverter.AttachmentToAPIAttachment(context.Background(), testAttachment)
|
||||||
|
@ -2996,6 +3121,244 @@ func (suite *InternalToFrontendTestSuite) TestRelationshipFollowRequested() {
|
||||||
}`, string(b))
|
}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *InternalToFrontendTestSuite) TestIntReqToAPI() {
|
||||||
|
requestingAccount := suite.testAccounts["local_account_2"]
|
||||||
|
adminReport, err := suite.typeconverter.InteractionReqToAPIInteractionReq(
|
||||||
|
context.Background(),
|
||||||
|
suite.testInteractionRequests["admin_account_reply_turtle"],
|
||||||
|
requestingAccount,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.MarshalIndent(adminReport, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Equal(`{
|
||||||
|
"id": "01J5QVXCCEATJYSXM9H6MZT4JR",
|
||||||
|
"type": "reply",
|
||||||
|
"created_at": "2024-02-20T10:41:37.000Z",
|
||||||
|
"account": {
|
||||||
|
"id": "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
|
"username": "admin",
|
||||||
|
"acct": "admin",
|
||||||
|
"display_name": "",
|
||||||
|
"locked": false,
|
||||||
|
"discoverable": true,
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2022-05-17T13:10:59.000Z",
|
||||||
|
"note": "",
|
||||||
|
"url": "http://localhost:8080/@admin",
|
||||||
|
"avatar": "",
|
||||||
|
"avatar_static": "",
|
||||||
|
"header": "http://localhost:8080/assets/default_header.webp",
|
||||||
|
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||||
|
"followers_count": 1,
|
||||||
|
"following_count": 1,
|
||||||
|
"statuses_count": 4,
|
||||||
|
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"enable_rss": true,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"id": "admin",
|
||||||
|
"name": "admin",
|
||||||
|
"color": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"id": "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||||
|
"created_at": "2021-10-20T10:40:37.000Z",
|
||||||
|
"in_reply_to_id": null,
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
|
"sensitive": true,
|
||||||
|
"spoiler_text": "you won't be able to reply to this without my approval",
|
||||||
|
"visibility": "unlisted",
|
||||||
|
"language": "en",
|
||||||
|
"uri": "http://localhost:8080/users/1happyturtle/statuses/01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||||
|
"url": "http://localhost:8080/@1happyturtle/statuses/01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||||
|
"replies_count": 1,
|
||||||
|
"reblogs_count": 0,
|
||||||
|
"favourites_count": 0,
|
||||||
|
"favourited": false,
|
||||||
|
"reblogged": false,
|
||||||
|
"muted": false,
|
||||||
|
"bookmarked": false,
|
||||||
|
"pinned": false,
|
||||||
|
"content": "🐢 i don't mind people sharing and liking this one but I want to moderate replies to it 🐢",
|
||||||
|
"reblog": null,
|
||||||
|
"application": {
|
||||||
|
"name": "kindaweird",
|
||||||
|
"website": "https://kindaweird.app"
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
|
"username": "1happyturtle",
|
||||||
|
"acct": "1happyturtle",
|
||||||
|
"display_name": "happy little turtle :3",
|
||||||
|
"locked": true,
|
||||||
|
"discoverable": false,
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2022-06-04T13:12:00.000Z",
|
||||||
|
"note": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
|
||||||
|
"url": "http://localhost:8080/@1happyturtle",
|
||||||
|
"avatar": "",
|
||||||
|
"avatar_static": "",
|
||||||
|
"header": "http://localhost:8080/assets/default_header.webp",
|
||||||
|
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||||
|
"followers_count": 1,
|
||||||
|
"following_count": 1,
|
||||||
|
"statuses_count": 8,
|
||||||
|
"last_status_at": "2021-07-28T08:40:37.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "should you follow me?",
|
||||||
|
"value": "maybe!",
|
||||||
|
"verified_at": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "age",
|
||||||
|
"value": "120",
|
||||||
|
"verified_at": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hide_collections": true
|
||||||
|
},
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [],
|
||||||
|
"tags": [],
|
||||||
|
"emojis": [],
|
||||||
|
"card": null,
|
||||||
|
"poll": null,
|
||||||
|
"text": "🐢 i don't mind people sharing and liking this one but I want to moderate replies to it 🐢",
|
||||||
|
"interaction_policy": {
|
||||||
|
"can_favourite": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reply": {
|
||||||
|
"always": [
|
||||||
|
"author",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": [
|
||||||
|
"public"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"can_reblog": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reply": {
|
||||||
|
"id": "01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||||
|
"created_at": "2024-02-20T10:41:37.000Z",
|
||||||
|
"in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||||
|
"in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"visibility": "unlisted",
|
||||||
|
"language": null,
|
||||||
|
"uri": "http://localhost:8080/users/admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||||
|
"url": "http://localhost:8080/@admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||||
|
"replies_count": 0,
|
||||||
|
"reblogs_count": 0,
|
||||||
|
"favourites_count": 0,
|
||||||
|
"favourited": false,
|
||||||
|
"reblogged": false,
|
||||||
|
"muted": false,
|
||||||
|
"bookmarked": false,
|
||||||
|
"pinned": false,
|
||||||
|
"content": "\u003cp\u003eHi \u003cspan class=\"h-card\"\u003e\u003ca href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e@\u003cspan\u003e1happyturtle\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e, can I reply?\u003c/p\u003e",
|
||||||
|
"reblog": null,
|
||||||
|
"application": {
|
||||||
|
"name": "superseriousbusiness",
|
||||||
|
"website": "https://superserious.business"
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
|
"username": "admin",
|
||||||
|
"acct": "admin",
|
||||||
|
"display_name": "",
|
||||||
|
"locked": false,
|
||||||
|
"discoverable": true,
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2022-05-17T13:10:59.000Z",
|
||||||
|
"note": "",
|
||||||
|
"url": "http://localhost:8080/@admin",
|
||||||
|
"avatar": "",
|
||||||
|
"avatar_static": "",
|
||||||
|
"header": "http://localhost:8080/assets/default_header.webp",
|
||||||
|
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||||
|
"followers_count": 1,
|
||||||
|
"following_count": 1,
|
||||||
|
"statuses_count": 4,
|
||||||
|
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"enable_rss": true,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"id": "admin",
|
||||||
|
"name": "admin",
|
||||||
|
"color": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [
|
||||||
|
{
|
||||||
|
"id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
|
"username": "1happyturtle",
|
||||||
|
"url": "http://localhost:8080/@1happyturtle",
|
||||||
|
"acct": "1happyturtle"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [],
|
||||||
|
"emojis": [],
|
||||||
|
"card": null,
|
||||||
|
"poll": null,
|
||||||
|
"text": "Hi @1happyturtle, can I reply?",
|
||||||
|
"interaction_policy": {
|
||||||
|
"can_favourite": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reply": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
},
|
||||||
|
"can_reblog": {
|
||||||
|
"always": [
|
||||||
|
"public",
|
||||||
|
"me"
|
||||||
|
],
|
||||||
|
"with_approval": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
func TestInternalToFrontendTestSuite(t *testing.T) {
|
func TestInternalToFrontendTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(InternalToFrontendTestSuite))
|
suite.Run(t, new(InternalToFrontendTestSuite))
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package typeutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -30,6 +31,8 @@ import (
|
||||||
"github.com/k3a/html2text"
|
"github.com/k3a/html2text"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/language"
|
"github.com/superseriousbusiness/gotosocial/internal/language"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -187,6 +190,47 @@ func placeholderAttachments(arr []*apimodel.Attachment) (string, []*apimodel.Att
|
||||||
return text.SanitizeToHTML(note.String()), arr
|
return text.SanitizeToHTML(note.String()), arr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Converter) pendingReplyNote(
|
||||||
|
ctx context.Context,
|
||||||
|
s *gtsmodel.Status,
|
||||||
|
) (string, error) {
|
||||||
|
intReq, err := c.state.DB.GetInteractionRequestByInteractionURI(ctx, s.URI)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
// Something's gone wrong.
|
||||||
|
err := gtserror.Newf("db error getting interaction request for %s: %w", s.URI, err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// No interaction request present
|
||||||
|
// for this status. Race condition?
|
||||||
|
if intReq == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
proto = config.GetProtocol()
|
||||||
|
host = config.GetHost()
|
||||||
|
|
||||||
|
// Build the settings panel URL at which the user
|
||||||
|
// can view + approve/reject the interaction request.
|
||||||
|
//
|
||||||
|
// Eg., https://example.org/settings/user/interaction_requests/01J5QVXCCEATJYSXM9H6MZT4JR
|
||||||
|
settingsURL = proto + "://" + host + "/settings/user/interaction_requests/" + intReq.ID
|
||||||
|
)
|
||||||
|
|
||||||
|
var note strings.Builder
|
||||||
|
note.WriteString(`<hr>`)
|
||||||
|
note.WriteString(`<p><i lang="en">ℹ️ Note from ` + host + `: `)
|
||||||
|
note.WriteString(`This reply is pending your approval. You can quickly accept it by liking, boosting or replying to it. You can also accept or reject it at the following link: `)
|
||||||
|
note.WriteString(`<a href="` + settingsURL + `" `)
|
||||||
|
note.WriteString(`rel="noreferrer noopener" target="_blank">`)
|
||||||
|
note.WriteString(settingsURL)
|
||||||
|
note.WriteString(`</a>.`)
|
||||||
|
note.WriteString(`</i></p>`)
|
||||||
|
|
||||||
|
return text.SanitizeToHTML(note.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ContentToContentLanguage tries to
|
// ContentToContentLanguage tries to
|
||||||
// extract a content string and language
|
// extract a content string and language
|
||||||
// tag string from the given intermediary
|
// tag string from the given intermediary
|
||||||
|
|
|
@ -844,9 +844,7 @@ func (h *histogram) Write(out *dto.Metric) error {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If exemplars are not configured, the cap will be 0.
|
if h.nativeExemplars.isEnabled() {
|
||||||
// So append is not needed in this case.
|
|
||||||
if cap(h.nativeExemplars.exemplars) > 0 {
|
|
||||||
h.nativeExemplars.Lock()
|
h.nativeExemplars.Lock()
|
||||||
his.Exemplars = append(his.Exemplars, h.nativeExemplars.exemplars...)
|
his.Exemplars = append(his.Exemplars, h.nativeExemplars.exemplars...)
|
||||||
h.nativeExemplars.Unlock()
|
h.nativeExemplars.Unlock()
|
||||||
|
@ -1665,6 +1663,10 @@ type nativeExemplars struct {
|
||||||
exemplars []*dto.Exemplar
|
exemplars []*dto.Exemplar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *nativeExemplars) isEnabled() bool {
|
||||||
|
return n.ttl != -1
|
||||||
|
}
|
||||||
|
|
||||||
func makeNativeExemplars(ttl time.Duration, maxCount int) nativeExemplars {
|
func makeNativeExemplars(ttl time.Duration, maxCount int) nativeExemplars {
|
||||||
if ttl == 0 {
|
if ttl == 0 {
|
||||||
ttl = 5 * time.Minute
|
ttl = 5 * time.Minute
|
||||||
|
@ -1686,7 +1688,7 @@ func makeNativeExemplars(ttl time.Duration, maxCount int) nativeExemplars {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
|
func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
|
||||||
if n.ttl == -1 {
|
if !n.isEnabled() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,12 +64,12 @@ issues:
|
||||||
- path: _test\.go
|
- path: _test\.go
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
# Ignoring gosec G404: Use of weak random number generator (math/rand instead of crypto/rand)
|
# Igonoring gosec G404: Use of weak random number generator (math/rand instead of crypto/rand)
|
||||||
# as we commonly use it in tests and examples.
|
# as we commonly use it in tests and examples.
|
||||||
- text: "G404:"
|
- text: "G404:"
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
# Ignoring gosec G402: TLS MinVersion too low
|
# Igonoring gosec G402: TLS MinVersion too low
|
||||||
# as the https://pkg.go.dev/crypto/tls#Config handles MinVersion default well.
|
# as the https://pkg.go.dev/crypto/tls#Config handles MinVersion default well.
|
||||||
- text: "G402: TLS MinVersion too low."
|
- text: "G402: TLS MinVersion too low."
|
||||||
linters:
|
linters:
|
||||||
|
|
|
@ -11,25 +11,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||||
<!-- Released section -->
|
<!-- Released section -->
|
||||||
<!-- Don't change this section unless doing release -->
|
<!-- Don't change this section unless doing release -->
|
||||||
|
|
||||||
## [1.30.0/0.52.0/0.6.0/0.0.9] 2024-09-09
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Support `OTEL_EXPORTER_OTLP_LOGS_INSECURE` and `OTEL_EXPORTER_OTLP_INSECURE` environments in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. (#5739)
|
|
||||||
- The `WithResource` option for `NewMeterProvider` now merges the provided resources with the ones from environment variables. (#5773)
|
|
||||||
- The `WithResource` option for `NewLoggerProvider` now merges the provided resources with the ones from environment variables. (#5773)
|
|
||||||
- Add UTF-8 support to `go.opentelemetry.io/otel/exporters/prometheus`. (#5755)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix memory leak in the global `MeterProvider` when identical instruments are repeatedly created. (#5754)
|
|
||||||
- Fix panic on instruments creation when setting meter provider. (#5758)
|
|
||||||
- Fix an issue where `SetMeterProvider` in `go.opentelemetry.io/otel` might miss the delegation for instruments and registries. (#5780)
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Drop support for [Go 1.21]. (#5736, #5740, #5800)
|
|
||||||
|
|
||||||
## [1.29.0/0.51.0/0.5.0] 2024-08-23
|
## [1.29.0/0.51.0/0.5.0] 2024-08-23
|
||||||
|
|
||||||
This release is the last to support [Go 1.21].
|
This release is the last to support [Go 1.21].
|
||||||
|
@ -1914,7 +1895,7 @@ with major version 0.
|
||||||
- Setting error status while recording error with Span from oteltest package. (#1729)
|
- Setting error status while recording error with Span from oteltest package. (#1729)
|
||||||
- The concept of a remote and local Span stored in a context is unified to just the current Span.
|
- The concept of a remote and local Span stored in a context is unified to just the current Span.
|
||||||
Because of this `"go.opentelemetry.io/otel/trace".RemoteSpanContextFromContext` is removed as it is no longer needed.
|
Because of this `"go.opentelemetry.io/otel/trace".RemoteSpanContextFromContext` is removed as it is no longer needed.
|
||||||
Instead, `"go.opentelemetry.io/otel/trace".SpanContextFromContext` can be used to return the current Span.
|
Instead, `"go.opentelemetry.io/otel/trace".SpanContextFromContex` can be used to return the current Span.
|
||||||
If needed, that Span's `SpanContext.IsRemote()` can then be used to determine if it is remote or not. (#1731)
|
If needed, that Span's `SpanContext.IsRemote()` can then be used to determine if it is remote or not. (#1731)
|
||||||
- The `HasRemoteParent` field of the `"go.opentelemetry.io/otel/sdk/trace".SamplingParameters` is removed.
|
- The `HasRemoteParent` field of the `"go.opentelemetry.io/otel/sdk/trace".SamplingParameters` is removed.
|
||||||
This field is redundant to the information returned from the `Remote` method of the `SpanContext` held in the `ParentContext` field. (#1749)
|
This field is redundant to the information returned from the `Remote` method of the `SpanContext` held in the `ParentContext` field. (#1749)
|
||||||
|
@ -2488,7 +2469,7 @@ This release migrates the default OpenTelemetry SDK into its own Go module, deco
|
||||||
- Prometheus exporter will not apply stale updates or forget inactive metrics. (#903)
|
- Prometheus exporter will not apply stale updates or forget inactive metrics. (#903)
|
||||||
- Add test for api.standard `HTTPClientAttributesFromHTTPRequest`. (#905)
|
- Add test for api.standard `HTTPClientAttributesFromHTTPRequest`. (#905)
|
||||||
- Bump github.com/golangci/golangci-lint from 1.27.0 to 1.28.1 in /tools. (#901, #913)
|
- Bump github.com/golangci/golangci-lint from 1.27.0 to 1.28.1 in /tools. (#901, #913)
|
||||||
- Update otel-collector example to use the v0.5.0 collector. (#915)
|
- Update otel-colector example to use the v0.5.0 collector. (#915)
|
||||||
- The `grpctrace` instrumentation uses a span name conforming to the OpenTelemetry semantic conventions (does not contain a leading slash (`/`)). (#922)
|
- The `grpctrace` instrumentation uses a span name conforming to the OpenTelemetry semantic conventions (does not contain a leading slash (`/`)). (#922)
|
||||||
- The `grpctrace` instrumentation includes an `rpc.method` attribute now set to the gRPC method name. (#900, #922)
|
- The `grpctrace` instrumentation includes an `rpc.method` attribute now set to the gRPC method name. (#900, #922)
|
||||||
- The `grpctrace` instrumentation `rpc.service` attribute now contains the package name if one exists.
|
- The `grpctrace` instrumentation `rpc.service` attribute now contains the package name if one exists.
|
||||||
|
@ -3081,8 +3062,7 @@ It contains api and sdk for trace and meter.
|
||||||
- CircleCI build CI manifest files.
|
- CircleCI build CI manifest files.
|
||||||
- CODEOWNERS file to track owners of this project.
|
- CODEOWNERS file to track owners of this project.
|
||||||
|
|
||||||
[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.30.0...HEAD
|
[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.29.0...HEAD
|
||||||
[1.30.0/0.52.0/0.6.0/0.0.9]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.30.0
|
|
||||||
[1.29.0/0.51.0/0.5.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.29.0
|
[1.29.0/0.51.0/0.5.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.29.0
|
||||||
[1.28.0/0.50.0/0.4.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.28.0
|
[1.28.0/0.50.0/0.4.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.28.0
|
||||||
[1.27.0/0.49.0/0.3.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.27.0
|
[1.27.0/0.49.0/0.3.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.27.0
|
||||||
|
|
|
@ -578,10 +578,7 @@ See also:
|
||||||
The tests should never leak goroutines.
|
The tests should never leak goroutines.
|
||||||
|
|
||||||
Use the term `ConcurrentSafe` in the test name when it aims to verify the
|
Use the term `ConcurrentSafe` in the test name when it aims to verify the
|
||||||
absence of race conditions. The top-level tests with this term will be run
|
absence of race conditions.
|
||||||
many times in the `test-concurrent-safe` CI job to increase the chance of
|
|
||||||
catching concurrency issues. This does not apply to subtests when this term
|
|
||||||
is not in their root name.
|
|
||||||
|
|
||||||
### Internal packages
|
### Internal packages
|
||||||
|
|
||||||
|
|
|
@ -145,14 +145,12 @@ build-tests/%:
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
|
|
||||||
TEST_TARGETS := test-default test-bench test-short test-verbose test-race test-concurrent-safe
|
TEST_TARGETS := test-default test-bench test-short test-verbose test-race
|
||||||
.PHONY: $(TEST_TARGETS) test
|
.PHONY: $(TEST_TARGETS) test
|
||||||
test-default test-race: ARGS=-race
|
test-default test-race: ARGS=-race
|
||||||
test-bench: ARGS=-run=xxxxxMatchNothingxxxxx -test.benchtime=1ms -bench=.
|
test-bench: ARGS=-run=xxxxxMatchNothingxxxxx -test.benchtime=1ms -bench=.
|
||||||
test-short: ARGS=-short
|
test-short: ARGS=-short
|
||||||
test-verbose: ARGS=-v -race
|
test-verbose: ARGS=-v -race
|
||||||
test-concurrent-safe: ARGS=-run=ConcurrentSafe -count=100 -race
|
|
||||||
test-concurrent-safe: TIMEOUT=120
|
|
||||||
$(TEST_TARGETS): test
|
$(TEST_TARGETS): test
|
||||||
test: $(OTEL_GO_MOD_DIRS:%=test/%)
|
test: $(OTEL_GO_MOD_DIRS:%=test/%)
|
||||||
test/%: DIR=$*
|
test/%: DIR=$*
|
||||||
|
|
|
@ -51,18 +51,25 @@ Currently, this project supports the following environments.
|
||||||
|----------|------------|--------------|
|
|----------|------------|--------------|
|
||||||
| Ubuntu | 1.23 | amd64 |
|
| Ubuntu | 1.23 | amd64 |
|
||||||
| Ubuntu | 1.22 | amd64 |
|
| Ubuntu | 1.22 | amd64 |
|
||||||
|
| Ubuntu | 1.21 | amd64 |
|
||||||
| Ubuntu | 1.23 | 386 |
|
| Ubuntu | 1.23 | 386 |
|
||||||
| Ubuntu | 1.22 | 386 |
|
| Ubuntu | 1.22 | 386 |
|
||||||
|
| Ubuntu | 1.21 | 386 |
|
||||||
| Linux | 1.23 | arm64 |
|
| Linux | 1.23 | arm64 |
|
||||||
| Linux | 1.22 | arm64 |
|
| Linux | 1.22 | arm64 |
|
||||||
|
| Linux | 1.21 | arm64 |
|
||||||
| macOS 13 | 1.23 | amd64 |
|
| macOS 13 | 1.23 | amd64 |
|
||||||
| macOS 13 | 1.22 | amd64 |
|
| macOS 13 | 1.22 | amd64 |
|
||||||
|
| macOS 13 | 1.21 | amd64 |
|
||||||
| macOS | 1.23 | arm64 |
|
| macOS | 1.23 | arm64 |
|
||||||
| macOS | 1.22 | arm64 |
|
| macOS | 1.22 | arm64 |
|
||||||
|
| macOS | 1.21 | arm64 |
|
||||||
| Windows | 1.23 | amd64 |
|
| Windows | 1.23 | amd64 |
|
||||||
| Windows | 1.22 | amd64 |
|
| Windows | 1.22 | amd64 |
|
||||||
|
| Windows | 1.21 | amd64 |
|
||||||
| Windows | 1.23 | 386 |
|
| Windows | 1.23 | 386 |
|
||||||
| Windows | 1.22 | 386 |
|
| Windows | 1.22 | 386 |
|
||||||
|
| Windows | 1.21 | 386 |
|
||||||
|
|
||||||
While this project should work for other systems, no compatibility guarantees
|
While this project should work for other systems, no compatibility guarantees
|
||||||
are made for those systems currently.
|
are made for those systems currently.
|
||||||
|
|
|
@ -50,7 +50,7 @@ type Property struct {
|
||||||
// component boundaries may impose their own restrictions on Property key.
|
// component boundaries may impose their own restrictions on Property key.
|
||||||
// For example, the W3C Baggage specification restricts the Property keys to strings that
|
// For example, the W3C Baggage specification restricts the Property keys to strings that
|
||||||
// satisfy the token definition from RFC7230, Section 3.2.6.
|
// satisfy the token definition from RFC7230, Section 3.2.6.
|
||||||
// For maximum compatibility, alphanumeric value are strongly recommended to be used as Property key.
|
// For maximum compatibility, alpha-numeric value are strongly recommended to be used as Property key.
|
||||||
func NewKeyProperty(key string) (Property, error) {
|
func NewKeyProperty(key string) (Property, error) {
|
||||||
if !validateBaggageName(key) {
|
if !validateBaggageName(key) {
|
||||||
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
|
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
|
||||||
|
@ -90,7 +90,7 @@ func NewKeyValueProperty(key, value string) (Property, error) {
|
||||||
// component boundaries may impose their own restrictions on Property key.
|
// component boundaries may impose their own restrictions on Property key.
|
||||||
// For example, the W3C Baggage specification restricts the Property keys to strings that
|
// For example, the W3C Baggage specification restricts the Property keys to strings that
|
||||||
// satisfy the token definition from RFC7230, Section 3.2.6.
|
// satisfy the token definition from RFC7230, Section 3.2.6.
|
||||||
// For maximum compatibility, alphanumeric value are strongly recommended to be used as Property key.
|
// For maximum compatibility, alpha-numeric value are strongly recommended to be used as Property key.
|
||||||
func NewKeyValuePropertyRaw(key, value string) (Property, error) {
|
func NewKeyValuePropertyRaw(key, value string) (Property, error) {
|
||||||
if !validateBaggageName(key) {
|
if !validateBaggageName(key) {
|
||||||
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
|
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
|
||||||
|
@ -287,7 +287,7 @@ func NewMember(key, value string, props ...Property) (Member, error) {
|
||||||
// component boundaries may impose their own restrictions on baggage key.
|
// component boundaries may impose their own restrictions on baggage key.
|
||||||
// For example, the W3C Baggage specification restricts the baggage keys to strings that
|
// For example, the W3C Baggage specification restricts the baggage keys to strings that
|
||||||
// satisfy the token definition from RFC7230, Section 3.2.6.
|
// satisfy the token definition from RFC7230, Section 3.2.6.
|
||||||
// For maximum compatibility, alphanumeric value are strongly recommended to be used as baggage key.
|
// For maximum compatibility, alpha-numeric value are strongly recommended to be used as baggage key.
|
||||||
func NewMemberRaw(key, value string, props ...Property) (Member, error) {
|
func NewMemberRaw(key, value string, props ...Property) (Member, error) {
|
||||||
m := Member{
|
m := Member{
|
||||||
key: key,
|
key: key,
|
||||||
|
|
|
@ -125,7 +125,7 @@ func NewGRPCConfig(opts ...GRPCOption) Config {
|
||||||
if cfg.ServiceConfig != "" {
|
if cfg.ServiceConfig != "" {
|
||||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultServiceConfig(cfg.ServiceConfig))
|
cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultServiceConfig(cfg.ServiceConfig))
|
||||||
}
|
}
|
||||||
// Prioritize GRPCCredentials over Insecure (passing both is an error).
|
// Priroritize GRPCCredentials over Insecure (passing both is an error).
|
||||||
if cfg.Traces.GRPCCredentials != nil {
|
if cfg.Traces.GRPCCredentials != nil {
|
||||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(cfg.Traces.GRPCCredentials))
|
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(cfg.Traces.GRPCCredentials))
|
||||||
} else if cfg.Traces.Insecure {
|
} else if cfg.Traces.Insecure {
|
||||||
|
|
|
@ -22,7 +22,7 @@ target URL to which the exporter sends telemetry.
|
||||||
The value must contain a scheme ("http" or "https") and host.
|
The value must contain a scheme ("http" or "https") and host.
|
||||||
The value may additionally contain a port and a path.
|
The value may additionally contain a port and a path.
|
||||||
The value should not contain a query string or fragment.
|
The value should not contain a query string or fragment.
|
||||||
The configuration can be overridden by [WithEndpoint], [WithEndpointURL], [WithInsecure], and [WithURLPath] options.
|
The configuration can be overridden by [WithEndpoint], [WithEndpointURL], [WitnInsecure], and [WithURLPath] options.
|
||||||
|
|
||||||
OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TRACES_HEADERS (default: none) -
|
OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TRACES_HEADERS (default: none) -
|
||||||
key-value pairs used as headers associated with HTTP requests.
|
key-value pairs used as headers associated with HTTP requests.
|
||||||
|
|
|
@ -125,7 +125,7 @@ func NewGRPCConfig(opts ...GRPCOption) Config {
|
||||||
if cfg.ServiceConfig != "" {
|
if cfg.ServiceConfig != "" {
|
||||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultServiceConfig(cfg.ServiceConfig))
|
cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultServiceConfig(cfg.ServiceConfig))
|
||||||
}
|
}
|
||||||
// Prioritize GRPCCredentials over Insecure (passing both is an error).
|
// Priroritize GRPCCredentials over Insecure (passing both is an error).
|
||||||
if cfg.Traces.GRPCCredentials != nil {
|
if cfg.Traces.GRPCCredentials != nil {
|
||||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(cfg.Traces.GRPCCredentials))
|
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(cfg.Traces.GRPCCredentials))
|
||||||
} else if cfg.Traces.Insecure {
|
} else if cfg.Traces.Insecure {
|
||||||
|
|
|
@ -5,5 +5,5 @@ package otlptrace // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||||
|
|
||||||
// Version is the current release version of the OpenTelemetry OTLP trace exporter in use.
|
// Version is the current release version of the OpenTelemetry OTLP trace exporter in use.
|
||||||
func Version() string {
|
func Version() string {
|
||||||
return "1.30.0"
|
return "1.29.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/common/model"
|
|
||||||
|
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/sdk/metric"
|
"go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
@ -132,10 +131,7 @@ func WithoutScopeInfo() Option {
|
||||||
// have special behavior based on their name.
|
// have special behavior based on their name.
|
||||||
func WithNamespace(ns string) Option {
|
func WithNamespace(ns string) Option {
|
||||||
return optionFunc(func(cfg config) config {
|
return optionFunc(func(cfg config) config {
|
||||||
if model.NameValidationScheme != model.UTF8Validation {
|
ns = sanitizeName(ns)
|
||||||
// Only sanitize if prometheus does not support UTF-8.
|
|
||||||
ns = model.EscapeName(ns, model.NameEscapingScheme)
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(ns, "_") {
|
if !strings.HasSuffix(ns, "_") {
|
||||||
// namespace and metric names should be separated with an underscore,
|
// namespace and metric names should be separated with an underscore,
|
||||||
// adds a trailing underscore if there is not one already.
|
// adds a trailing underscore if there is not one already.
|
||||||
|
|
|
@ -11,10 +11,11 @@ import (
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
dto "github.com/prometheus/client_model/go"
|
dto "github.com/prometheus/client_model/go"
|
||||||
"github.com/prometheus/common/model"
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
|
@ -297,38 +298,28 @@ func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metric
|
||||||
}
|
}
|
||||||
|
|
||||||
// getAttrs parses the attribute.Set to two lists of matching Prometheus-style
|
// getAttrs parses the attribute.Set to two lists of matching Prometheus-style
|
||||||
// keys and values.
|
// keys and values. It sanitizes invalid characters and handles duplicate keys
|
||||||
|
// (due to sanitization) by sorting and concatenating the values following the spec.
|
||||||
func getAttrs(attrs attribute.Set, ks, vs [2]string, resourceKV keyVals) ([]string, []string) {
|
func getAttrs(attrs attribute.Set, ks, vs [2]string, resourceKV keyVals) ([]string, []string) {
|
||||||
|
keysMap := make(map[string][]string)
|
||||||
|
itr := attrs.Iter()
|
||||||
|
for itr.Next() {
|
||||||
|
kv := itr.Attribute()
|
||||||
|
key := strings.Map(sanitizeRune, string(kv.Key))
|
||||||
|
if _, ok := keysMap[key]; !ok {
|
||||||
|
keysMap[key] = []string{kv.Value.Emit()}
|
||||||
|
} else {
|
||||||
|
// if the sanitized key is a duplicate, append to the list of keys
|
||||||
|
keysMap[key] = append(keysMap[key], kv.Value.Emit())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
keys := make([]string, 0, attrs.Len())
|
keys := make([]string, 0, attrs.Len())
|
||||||
values := make([]string, 0, attrs.Len())
|
values := make([]string, 0, attrs.Len())
|
||||||
itr := attrs.Iter()
|
for key, vals := range keysMap {
|
||||||
|
keys = append(keys, key)
|
||||||
if model.NameValidationScheme == model.UTF8Validation {
|
slices.Sort(vals)
|
||||||
// Do not perform sanitization if prometheus supports UTF-8.
|
values = append(values, strings.Join(vals, ";"))
|
||||||
for itr.Next() {
|
|
||||||
kv := itr.Attribute()
|
|
||||||
keys = append(keys, string(kv.Key))
|
|
||||||
values = append(values, kv.Value.Emit())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// It sanitizes invalid characters and handles duplicate keys
|
|
||||||
// (due to sanitization) by sorting and concatenating the values following the spec.
|
|
||||||
keysMap := make(map[string][]string)
|
|
||||||
for itr.Next() {
|
|
||||||
kv := itr.Attribute()
|
|
||||||
key := model.EscapeName(string(kv.Key), model.NameEscapingScheme)
|
|
||||||
if _, ok := keysMap[key]; !ok {
|
|
||||||
keysMap[key] = []string{kv.Value.Emit()}
|
|
||||||
} else {
|
|
||||||
// if the sanitized key is a duplicate, append to the list of keys
|
|
||||||
keysMap[key] = append(keysMap[key], kv.Value.Emit())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for key, vals := range keysMap {
|
|
||||||
keys = append(keys, key)
|
|
||||||
slices.Sort(vals)
|
|
||||||
values = append(values, strings.Join(vals, ";"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ks[0] != "" {
|
if ks[0] != "" {
|
||||||
|
@ -356,6 +347,13 @@ func createScopeInfoMetric(scope instrumentation.Scope) (prometheus.Metric, erro
|
||||||
return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(1), scope.Name, scope.Version)
|
return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(1), scope.Name, scope.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sanitizeRune(r rune) rune {
|
||||||
|
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == ':' || r == '_' {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return '_'
|
||||||
|
}
|
||||||
|
|
||||||
var unitSuffixes = map[string]string{
|
var unitSuffixes = map[string]string{
|
||||||
// Time
|
// Time
|
||||||
"d": "_days",
|
"d": "_days",
|
||||||
|
@ -394,11 +392,7 @@ var unitSuffixes = map[string]string{
|
||||||
|
|
||||||
// getName returns the sanitized name, prefixed with the namespace and suffixed with unit.
|
// getName returns the sanitized name, prefixed with the namespace and suffixed with unit.
|
||||||
func (c *collector) getName(m metricdata.Metrics, typ *dto.MetricType) string {
|
func (c *collector) getName(m metricdata.Metrics, typ *dto.MetricType) string {
|
||||||
name := m.Name
|
name := sanitizeName(m.Name)
|
||||||
if model.NameValidationScheme != model.UTF8Validation {
|
|
||||||
// Only sanitize if prometheus does not support UTF-8.
|
|
||||||
name = model.EscapeName(name, model.NameEscapingScheme)
|
|
||||||
}
|
|
||||||
addCounterSuffix := !c.withoutCounterSuffixes && *typ == dto.MetricType_COUNTER
|
addCounterSuffix := !c.withoutCounterSuffixes && *typ == dto.MetricType_COUNTER
|
||||||
if addCounterSuffix {
|
if addCounterSuffix {
|
||||||
// Remove the _total suffix here, as we will re-add the total suffix
|
// Remove the _total suffix here, as we will re-add the total suffix
|
||||||
|
@ -417,6 +411,59 @@ func (c *collector) getName(m metricdata.Metrics, typ *dto.MetricType) string {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sanitizeName(n string) string {
|
||||||
|
// This algorithm is based on strings.Map from Go 1.19.
|
||||||
|
const replacement = '_'
|
||||||
|
|
||||||
|
valid := func(i int, r rune) bool {
|
||||||
|
// Taken from
|
||||||
|
// https://github.com/prometheus/common/blob/dfbc25bd00225c70aca0d94c3c4bb7744f28ace0/model/metric.go#L92-L102
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_' || r == ':' || (r >= '0' && r <= '9' && i > 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// This output buffer b is initialized on demand, the first time a
|
||||||
|
// character needs to be replaced.
|
||||||
|
var b strings.Builder
|
||||||
|
for i, c := range n {
|
||||||
|
if valid(i, c) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 0 && c >= '0' && c <= '9' {
|
||||||
|
// Prefix leading number with replacement character.
|
||||||
|
b.Grow(len(n) + 1)
|
||||||
|
_ = b.WriteByte(byte(replacement))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b.Grow(len(n))
|
||||||
|
_, _ = b.WriteString(n[:i])
|
||||||
|
_ = b.WriteByte(byte(replacement))
|
||||||
|
width := utf8.RuneLen(c)
|
||||||
|
n = n[i+width:]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast path for unchanged input.
|
||||||
|
if b.Cap() == 0 { // b.Grow was not called above.
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range n {
|
||||||
|
// Due to inlining, it is more performant to invoke WriteByte rather then
|
||||||
|
// WriteRune.
|
||||||
|
if valid(1, c) { // We are guaranteed to not be at the start.
|
||||||
|
_ = b.WriteByte(byte(c))
|
||||||
|
} else {
|
||||||
|
_ = b.WriteByte(byte(replacement))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType {
|
func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType {
|
||||||
switch v := m.Data.(type) {
|
switch v := m.Data.(type) {
|
||||||
case metricdata.Histogram[int64], metricdata.Histogram[float64]:
|
case metricdata.Histogram[int64], metricdata.Histogram[float64]:
|
||||||
|
|
|
@ -5,8 +5,8 @@ package global // import "go.opentelemetry.io/otel/internal/global"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
"container/list"
|
||||||
"reflect"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel/metric"
|
"go.opentelemetry.io/otel/metric"
|
||||||
"go.opentelemetry.io/otel/metric/embedded"
|
"go.opentelemetry.io/otel/metric/embedded"
|
||||||
|
@ -76,7 +76,7 @@ func (p *meterProvider) Meter(name string, opts ...metric.MeterOption) metric.Me
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
t := &meter{name: name, opts: opts, instruments: make(map[instID]delegatedInstrument)}
|
t := &meter{name: name, opts: opts}
|
||||||
p.meters[key] = t
|
p.meters[key] = t
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
@ -92,29 +92,17 @@ type meter struct {
|
||||||
opts []metric.MeterOption
|
opts []metric.MeterOption
|
||||||
|
|
||||||
mtx sync.Mutex
|
mtx sync.Mutex
|
||||||
instruments map[instID]delegatedInstrument
|
instruments []delegatedInstrument
|
||||||
|
|
||||||
registry list.List
|
registry list.List
|
||||||
|
|
||||||
delegate metric.Meter
|
delegate atomic.Value // metric.Meter
|
||||||
}
|
}
|
||||||
|
|
||||||
type delegatedInstrument interface {
|
type delegatedInstrument interface {
|
||||||
setDelegate(metric.Meter)
|
setDelegate(metric.Meter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// instID are the identifying properties of a instrument.
|
|
||||||
type instID struct {
|
|
||||||
// name is the name of the stream.
|
|
||||||
name string
|
|
||||||
// description is the description of the stream.
|
|
||||||
description string
|
|
||||||
// kind defines the functional group of the instrument.
|
|
||||||
kind reflect.Type
|
|
||||||
// unit is the unit of the stream.
|
|
||||||
unit string
|
|
||||||
}
|
|
||||||
|
|
||||||
// setDelegate configures m to delegate all Meter functionality to Meters
|
// setDelegate configures m to delegate all Meter functionality to Meters
|
||||||
// created by provider.
|
// created by provider.
|
||||||
//
|
//
|
||||||
|
@ -122,12 +110,12 @@ type instID struct {
|
||||||
//
|
//
|
||||||
// It is guaranteed by the caller that this happens only once.
|
// It is guaranteed by the caller that this happens only once.
|
||||||
func (m *meter) setDelegate(provider metric.MeterProvider) {
|
func (m *meter) setDelegate(provider metric.MeterProvider) {
|
||||||
|
meter := provider.Meter(m.name, m.opts...)
|
||||||
|
m.delegate.Store(meter)
|
||||||
|
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
meter := provider.Meter(m.name, m.opts...)
|
|
||||||
m.delegate = meter
|
|
||||||
|
|
||||||
for _, inst := range m.instruments {
|
for _, inst := range m.instruments {
|
||||||
inst.setDelegate(meter)
|
inst.setDelegate(meter)
|
||||||
}
|
}
|
||||||
|
@ -145,295 +133,169 @@ func (m *meter) setDelegate(provider metric.MeterProvider) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Int64Counter(name string, options ...metric.Int64CounterOption) (metric.Int64Counter, error) {
|
func (m *meter) Int64Counter(name string, options ...metric.Int64CounterOption) (metric.Int64Counter, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Int64Counter(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Int64Counter(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &siCounter{name: name, opts: options}
|
i := &siCounter{name: name, opts: options}
|
||||||
cfg := metric.NewInt64CounterConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Int64UpDownCounter(name string, options ...metric.Int64UpDownCounterOption) (metric.Int64UpDownCounter, error) {
|
func (m *meter) Int64UpDownCounter(name string, options ...metric.Int64UpDownCounterOption) (metric.Int64UpDownCounter, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Int64UpDownCounter(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Int64UpDownCounter(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &siUpDownCounter{name: name, opts: options}
|
i := &siUpDownCounter{name: name, opts: options}
|
||||||
cfg := metric.NewInt64UpDownCounterConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Int64Histogram(name string, options ...metric.Int64HistogramOption) (metric.Int64Histogram, error) {
|
func (m *meter) Int64Histogram(name string, options ...metric.Int64HistogramOption) (metric.Int64Histogram, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Int64Histogram(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Int64Histogram(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &siHistogram{name: name, opts: options}
|
i := &siHistogram{name: name, opts: options}
|
||||||
cfg := metric.NewInt64HistogramConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Int64Gauge(name string, options ...metric.Int64GaugeOption) (metric.Int64Gauge, error) {
|
func (m *meter) Int64Gauge(name string, options ...metric.Int64GaugeOption) (metric.Int64Gauge, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Int64Gauge(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Int64Gauge(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &siGauge{name: name, opts: options}
|
i := &siGauge{name: name, opts: options}
|
||||||
cfg := metric.NewInt64GaugeConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Int64ObservableCounter(name string, options ...metric.Int64ObservableCounterOption) (metric.Int64ObservableCounter, error) {
|
func (m *meter) Int64ObservableCounter(name string, options ...metric.Int64ObservableCounterOption) (metric.Int64ObservableCounter, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Int64ObservableCounter(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Int64ObservableCounter(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &aiCounter{name: name, opts: options}
|
i := &aiCounter{name: name, opts: options}
|
||||||
cfg := metric.NewInt64ObservableCounterConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Int64ObservableUpDownCounter(name string, options ...metric.Int64ObservableUpDownCounterOption) (metric.Int64ObservableUpDownCounter, error) {
|
func (m *meter) Int64ObservableUpDownCounter(name string, options ...metric.Int64ObservableUpDownCounterOption) (metric.Int64ObservableUpDownCounter, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Int64ObservableUpDownCounter(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Int64ObservableUpDownCounter(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &aiUpDownCounter{name: name, opts: options}
|
i := &aiUpDownCounter{name: name, opts: options}
|
||||||
cfg := metric.NewInt64ObservableUpDownCounterConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Int64ObservableGauge(name string, options ...metric.Int64ObservableGaugeOption) (metric.Int64ObservableGauge, error) {
|
func (m *meter) Int64ObservableGauge(name string, options ...metric.Int64ObservableGaugeOption) (metric.Int64ObservableGauge, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Int64ObservableGauge(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Int64ObservableGauge(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &aiGauge{name: name, opts: options}
|
i := &aiGauge{name: name, opts: options}
|
||||||
cfg := metric.NewInt64ObservableGaugeConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Float64Counter(name string, options ...metric.Float64CounterOption) (metric.Float64Counter, error) {
|
func (m *meter) Float64Counter(name string, options ...metric.Float64CounterOption) (metric.Float64Counter, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Float64Counter(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Float64Counter(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &sfCounter{name: name, opts: options}
|
i := &sfCounter{name: name, opts: options}
|
||||||
cfg := metric.NewFloat64CounterConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Float64UpDownCounter(name string, options ...metric.Float64UpDownCounterOption) (metric.Float64UpDownCounter, error) {
|
func (m *meter) Float64UpDownCounter(name string, options ...metric.Float64UpDownCounterOption) (metric.Float64UpDownCounter, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Float64UpDownCounter(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Float64UpDownCounter(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &sfUpDownCounter{name: name, opts: options}
|
i := &sfUpDownCounter{name: name, opts: options}
|
||||||
cfg := metric.NewFloat64UpDownCounterConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Float64Histogram(name string, options ...metric.Float64HistogramOption) (metric.Float64Histogram, error) {
|
func (m *meter) Float64Histogram(name string, options ...metric.Float64HistogramOption) (metric.Float64Histogram, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Float64Histogram(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Float64Histogram(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &sfHistogram{name: name, opts: options}
|
i := &sfHistogram{name: name, opts: options}
|
||||||
cfg := metric.NewFloat64HistogramConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Float64Gauge(name string, options ...metric.Float64GaugeOption) (metric.Float64Gauge, error) {
|
func (m *meter) Float64Gauge(name string, options ...metric.Float64GaugeOption) (metric.Float64Gauge, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Float64Gauge(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Float64Gauge(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &sfGauge{name: name, opts: options}
|
i := &sfGauge{name: name, opts: options}
|
||||||
cfg := metric.NewFloat64GaugeConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Float64ObservableCounter(name string, options ...metric.Float64ObservableCounterOption) (metric.Float64ObservableCounter, error) {
|
func (m *meter) Float64ObservableCounter(name string, options ...metric.Float64ObservableCounterOption) (metric.Float64ObservableCounter, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Float64ObservableCounter(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Float64ObservableCounter(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &afCounter{name: name, opts: options}
|
i := &afCounter{name: name, opts: options}
|
||||||
cfg := metric.NewFloat64ObservableCounterConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Float64ObservableUpDownCounter(name string, options ...metric.Float64ObservableUpDownCounterOption) (metric.Float64ObservableUpDownCounter, error) {
|
func (m *meter) Float64ObservableUpDownCounter(name string, options ...metric.Float64ObservableUpDownCounterOption) (metric.Float64ObservableUpDownCounter, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Float64ObservableUpDownCounter(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Float64ObservableUpDownCounter(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &afUpDownCounter{name: name, opts: options}
|
i := &afUpDownCounter{name: name, opts: options}
|
||||||
cfg := metric.NewFloat64ObservableUpDownCounterConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *meter) Float64ObservableGauge(name string, options ...metric.Float64ObservableGaugeOption) (metric.Float64ObservableGauge, error) {
|
func (m *meter) Float64ObservableGauge(name string, options ...metric.Float64ObservableGaugeOption) (metric.Float64ObservableGauge, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
return del.Float64ObservableGauge(name, options...)
|
||||||
|
}
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
return m.delegate.Float64ObservableGauge(name, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
i := &afGauge{name: name, opts: options}
|
i := &afGauge{name: name, opts: options}
|
||||||
cfg := metric.NewFloat64ObservableGaugeConfig(options...)
|
m.instruments = append(m.instruments, i)
|
||||||
id := instID{
|
|
||||||
name: name,
|
|
||||||
kind: reflect.TypeOf(i),
|
|
||||||
description: cfg.Description(),
|
|
||||||
unit: cfg.Unit(),
|
|
||||||
}
|
|
||||||
m.instruments[id] = i
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterCallback captures the function that will be called during Collect.
|
// RegisterCallback captures the function that will be called during Collect.
|
||||||
func (m *meter) RegisterCallback(f metric.Callback, insts ...metric.Observable) (metric.Registration, error) {
|
func (m *meter) RegisterCallback(f metric.Callback, insts ...metric.Observable) (metric.Registration, error) {
|
||||||
|
if del, ok := m.delegate.Load().(metric.Meter); ok {
|
||||||
|
insts = unwrapInstruments(insts)
|
||||||
|
return del.RegisterCallback(f, insts...)
|
||||||
|
}
|
||||||
|
|
||||||
m.mtx.Lock()
|
m.mtx.Lock()
|
||||||
defer m.mtx.Unlock()
|
defer m.mtx.Unlock()
|
||||||
|
|
||||||
if m.delegate != nil {
|
|
||||||
insts = unwrapInstruments(insts)
|
|
||||||
return m.delegate.RegisterCallback(f, insts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
reg := ®istration{instruments: insts, function: f}
|
reg := ®istration{instruments: insts, function: f}
|
||||||
e := m.registry.PushBack(reg)
|
e := m.registry.PushBack(reg)
|
||||||
reg.unreg = func() error {
|
reg.unreg = func() error {
|
||||||
|
|
|
@ -213,7 +213,7 @@ type Float64Observer interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Float64Callback is a function registered with a Meter that makes
|
// Float64Callback is a function registered with a Meter that makes
|
||||||
// observations for a Float64Observable instrument it is registered with.
|
// observations for a Float64Observerable instrument it is registered with.
|
||||||
// Calls to the Float64Observer record measurement values for the
|
// Calls to the Float64Observer record measurement values for the
|
||||||
// Float64Observable.
|
// Float64Observable.
|
||||||
//
|
//
|
||||||
|
|
|
@ -212,7 +212,7 @@ type Int64Observer interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Int64Callback is a function registered with a Meter that makes observations
|
// Int64Callback is a function registered with a Meter that makes observations
|
||||||
// for an Int64Observable instrument it is registered with. Calls to the
|
// for an Int64Observerable instrument it is registered with. Calls to the
|
||||||
// Int64Observer record measurement values for the Int64Observable.
|
// Int64Observer record measurement values for the Int64Observable.
|
||||||
//
|
//
|
||||||
// The function needs to complete in a finite amount of time and the deadline
|
// The function needs to complete in a finite amount of time and the deadline
|
||||||
|
|
|
@ -19,10 +19,6 @@
|
||||||
"matchManagers": ["gomod"],
|
"matchManagers": ["gomod"],
|
||||||
"matchDepTypes": ["indirect"],
|
"matchDepTypes": ["indirect"],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchPackageNames": ["google.golang.org/genproto/googleapis/**"],
|
|
||||||
"groupName": "googleapis"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
"go.opentelemetry.io/otel/sdk/resource"
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -104,11 +103,7 @@ func (o optionFunc) apply(conf config) config {
|
||||||
// go.opentelemetry.io/otel/sdk/resource package will be used.
|
// go.opentelemetry.io/otel/sdk/resource package will be used.
|
||||||
func WithResource(res *resource.Resource) Option {
|
func WithResource(res *resource.Resource) Option {
|
||||||
return optionFunc(func(conf config) config {
|
return optionFunc(func(conf config) config {
|
||||||
var err error
|
conf.res = res
|
||||||
conf.res, err = resource.Merge(resource.Environment(), res)
|
|
||||||
if err != nil {
|
|
||||||
otel.Handle(err)
|
|
||||||
}
|
|
||||||
return conf
|
return conf
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -234,8 +234,8 @@ func (i *float64Inst) aggregate(ctx context.Context, val float64, s attribute.Se
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// observableID is a comparable unique identifier of an observable.
|
// observablID is a comparable unique identifier of an observable.
|
||||||
type observableID[N int64 | float64] struct {
|
type observablID[N int64 | float64] struct {
|
||||||
name string
|
name string
|
||||||
description string
|
description string
|
||||||
kind InstrumentKind
|
kind InstrumentKind
|
||||||
|
@ -287,7 +287,7 @@ func newInt64Observable(m *meter, kind InstrumentKind, name, desc, u string) int
|
||||||
|
|
||||||
type observable[N int64 | float64] struct {
|
type observable[N int64 | float64] struct {
|
||||||
metric.Observable
|
metric.Observable
|
||||||
observableID[N]
|
observablID[N]
|
||||||
|
|
||||||
meter *meter
|
meter *meter
|
||||||
measures measures[N]
|
measures measures[N]
|
||||||
|
@ -296,7 +296,7 @@ type observable[N int64 | float64] struct {
|
||||||
|
|
||||||
func newObservable[N int64 | float64](m *meter, kind InstrumentKind, name, desc, u string) *observable[N] {
|
func newObservable[N int64 | float64](m *meter, kind InstrumentKind, name, desc, u string) *observable[N] {
|
||||||
return &observable[N]{
|
return &observable[N]{
|
||||||
observableID: observableID[N]{
|
observablID: observablID[N]{
|
||||||
name: name,
|
name: name,
|
||||||
description: desc,
|
description: desc,
|
||||||
kind: kind,
|
kind: kind,
|
||||||
|
|
|
@ -142,7 +142,7 @@ func (s *sum[N]) cumulative(dest *metricdata.Aggregation) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// newPrecomputedSum returns an aggregator that summarizes a set of
|
// newPrecomputedSum returns an aggregator that summarizes a set of
|
||||||
// observations as their arithmetic sum. Each sum is scoped by attributes and
|
// observatrions as their arithmetic sum. Each sum is scoped by attributes and
|
||||||
// the aggregation cycle the measurements were made in.
|
// the aggregation cycle the measurements were made in.
|
||||||
func newPrecomputedSum[N int64 | float64](monotonic bool, limit int, r func() exemplar.FilteredReservoir[N]) *precomputedSum[N] {
|
func newPrecomputedSum[N int64 | float64](monotonic bool, limit int, r func() exemplar.FilteredReservoir[N]) *precomputedSum[N] {
|
||||||
return &precomputedSum[N]{
|
return &precomputedSum[N]{
|
||||||
|
@ -152,7 +152,7 @@ func newPrecomputedSum[N int64 | float64](monotonic bool, limit int, r func() ex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// precomputedSum summarizes a set of observations as their arithmetic sum.
|
// precomputedSum summarizes a set of observatrions as their arithmetic sum.
|
||||||
type precomputedSum[N int64 | float64] struct {
|
type precomputedSum[N int64 | float64] struct {
|
||||||
*valueMap[N]
|
*valueMap[N]
|
||||||
|
|
||||||
|
|
2
vendor/go.opentelemetry.io/otel/sdk/metric/internal/exemplar/filtered_reservoir.go
generated
vendored
|
@ -41,7 +41,7 @@ func NewFilteredReservoir[N int64 | float64](f Filter, r Reservoir) FilteredRese
|
||||||
|
|
||||||
func (f *filteredReservoir[N]) Offer(ctx context.Context, val N, attr []attribute.KeyValue) {
|
func (f *filteredReservoir[N]) Offer(ctx context.Context, val N, attr []attribute.KeyValue) {
|
||||||
if f.filter(ctx) {
|
if f.filter(ctx) {
|
||||||
// only record the current time if we are sampling this measurement.
|
// only record the current time if we are sampling this measurment.
|
||||||
f.reservoir.Offer(ctx, time.Now(), NewValue(val), attr)
|
f.reservoir.Offer(ctx, time.Now(), NewValue(val), attr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ var (
|
||||||
// Do not use crypto/rand. There is no reason for the decrease in performance
|
// Do not use crypto/rand. There is no reason for the decrease in performance
|
||||||
// given this is not a security sensitive decision.
|
// given this is not a security sensitive decision.
|
||||||
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
// Ensure concurrent safe access to rng and its underlying source.
|
// Ensure concurrent safe accecess to rng and its underlying source.
|
||||||
rngMu sync.Mutex
|
rngMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -459,7 +459,7 @@ func (m *meter) RegisterCallback(f metric.Callback, insts ...metric.Observable)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
reg.registerInt64(o.observableID)
|
reg.registerInt64(o.observablID)
|
||||||
case float64Observable:
|
case float64Observable:
|
||||||
if err := o.registerable(m); err != nil {
|
if err := o.registerable(m); err != nil {
|
||||||
if !errors.Is(err, errEmptyAgg) {
|
if !errors.Is(err, errEmptyAgg) {
|
||||||
|
@ -467,7 +467,7 @@ func (m *meter) RegisterCallback(f metric.Callback, insts ...metric.Observable)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
reg.registerFloat64(o.observableID)
|
reg.registerFloat64(o.observablID)
|
||||||
default:
|
default:
|
||||||
// Instrument external to the SDK.
|
// Instrument external to the SDK.
|
||||||
return nil, fmt.Errorf("invalid observable: from different implementation")
|
return nil, fmt.Errorf("invalid observable: from different implementation")
|
||||||
|
@ -488,14 +488,14 @@ func (m *meter) RegisterCallback(f metric.Callback, insts ...metric.Observable)
|
||||||
type observer struct {
|
type observer struct {
|
||||||
embedded.Observer
|
embedded.Observer
|
||||||
|
|
||||||
float64 map[observableID[float64]]struct{}
|
float64 map[observablID[float64]]struct{}
|
||||||
int64 map[observableID[int64]]struct{}
|
int64 map[observablID[int64]]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newObserver() observer {
|
func newObserver() observer {
|
||||||
return observer{
|
return observer{
|
||||||
float64: make(map[observableID[float64]]struct{}),
|
float64: make(map[observablID[float64]]struct{}),
|
||||||
int64: make(map[observableID[int64]]struct{}),
|
int64: make(map[observablID[int64]]struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -503,11 +503,11 @@ func (r observer) len() int {
|
||||||
return len(r.float64) + len(r.int64)
|
return len(r.float64) + len(r.int64)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r observer) registerFloat64(id observableID[float64]) {
|
func (r observer) registerFloat64(id observablID[float64]) {
|
||||||
r.float64[id] = struct{}{}
|
r.float64[id] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r observer) registerInt64(id observableID[int64]) {
|
func (r observer) registerInt64(id observablID[int64]) {
|
||||||
r.int64[id] = struct{}{}
|
r.int64[id] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -536,7 +536,7 @@ func (r observer) ObserveFloat64(o metric.Float64Observable, v float64, opts ...
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, registered := r.float64[oImpl.observableID]; !registered {
|
if _, registered := r.float64[oImpl.observablID]; !registered {
|
||||||
if !oImpl.dropAggregation {
|
if !oImpl.dropAggregation {
|
||||||
global.Error(errUnregObserver, "failed to record",
|
global.Error(errUnregObserver, "failed to record",
|
||||||
"name", oImpl.name,
|
"name", oImpl.name,
|
||||||
|
@ -571,7 +571,7 @@ func (r observer) ObserveInt64(o metric.Int64Observable, v int64, opts ...metric
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, registered := r.int64[oImpl.observableID]; !registered {
|
if _, registered := r.int64[oImpl.observablID]; !registered {
|
||||||
if !oImpl.dropAggregation {
|
if !oImpl.dropAggregation {
|
||||||
global.Error(errUnregObserver, "failed to record",
|
global.Error(errUnregObserver, "failed to record",
|
||||||
"name", oImpl.name,
|
"name", oImpl.name,
|
||||||
|
|
|
@ -5,5 +5,5 @@ package metric // import "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
|
||||||
// version is the current release version of the metric SDK in use.
|
// version is the current release version of the metric SDK in use.
|
||||||
func version() string {
|
func version() string {
|
||||||
return "1.30.0"
|
return "1.29.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,16 +10,17 @@ import (
|
||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// implements hostIDReader.
|
// implements hostIDReader
|
||||||
type hostIDReaderWindows struct{}
|
type hostIDReaderWindows struct{}
|
||||||
|
|
||||||
// read reads MachineGuid from the Windows registry key:
|
// read reads MachineGuid from the windows registry key:
|
||||||
// SOFTWARE\Microsoft\Cryptography.
|
// SOFTWARE\Microsoft\Cryptography
|
||||||
func (*hostIDReaderWindows) read() (string, error) {
|
func (*hostIDReaderWindows) read() (string, error) {
|
||||||
k, err := registry.OpenKey(
|
k, err := registry.OpenKey(
|
||||||
registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Cryptography`,
|
registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Cryptography`,
|
||||||
registry.QUERY_VALUE|registry.WOW64_64KEY,
|
registry.QUERY_VALUE|registry.WOW64_64KEY,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
func platformOSDescription() (string, error) {
|
func platformOSDescription() (string, error) {
|
||||||
k, err := registry.OpenKey(
|
k, err := registry.OpenKey(
|
||||||
registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
|
registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,5 +5,5 @@ package sdk // import "go.opentelemetry.io/otel/sdk"
|
||||||
|
|
||||||
// Version is the current release version of the OpenTelemetry SDK in use.
|
// Version is the current release version of the OpenTelemetry SDK in use.
|
||||||
func Version() string {
|
func Version() string {
|
||||||
return "1.30.0"
|
return "1.29.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ func ContextWithSpanContext(parent context.Context, sc SpanContext) context.Cont
|
||||||
return ContextWithSpan(parent, nonRecordingSpan{sc: sc})
|
return ContextWithSpan(parent, nonRecordingSpan{sc: sc})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextWithRemoteSpanContext returns a copy of parent with rsc set explicitly
|
// ContextWithRemoteSpanContext returns a copy of parent with rsc set explicly
|
||||||
// as a remote SpanContext and as the current Span. The Span implementation
|
// as a remote SpanContext and as the current Span. The Span implementation
|
||||||
// that wraps rsc is non-recording and performs no operations other than to
|
// that wraps rsc is non-recording and performs no operations other than to
|
||||||
// return rsc as the SpanContext from the SpanContext method.
|
// return rsc as the SpanContext from the SpanContext method.
|
||||||
|
|
|
@ -96,7 +96,7 @@ can embed the API interface directly.
|
||||||
|
|
||||||
This option is not recommended. It will lead to publishing packages that
|
This option is not recommended. It will lead to publishing packages that
|
||||||
contain runtime panics when users update to newer versions of
|
contain runtime panics when users update to newer versions of
|
||||||
[go.opentelemetry.io/otel/trace], which may be done with a transitive
|
[go.opentelemetry.io/otel/trace], which may be done with a trasitive
|
||||||
dependency.
|
dependency.
|
||||||
|
|
||||||
Finally, an author can embed another implementation in theirs. The embedded
|
Finally, an author can embed another implementation in theirs. The embedded
|
||||||
|
|
|
@ -5,5 +5,5 @@ package otel // import "go.opentelemetry.io/otel"
|
||||||
|
|
||||||
// Version is the current release version of OpenTelemetry in use.
|
// Version is the current release version of OpenTelemetry in use.
|
||||||
func Version() string {
|
func Version() string {
|
||||||
return "1.30.0"
|
return "1.29.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
module-sets:
|
module-sets:
|
||||||
stable-v1:
|
stable-v1:
|
||||||
version: v1.30.0
|
version: v1.29.0
|
||||||
modules:
|
modules:
|
||||||
- go.opentelemetry.io/otel
|
- go.opentelemetry.io/otel
|
||||||
- go.opentelemetry.io/otel/bridge/opencensus
|
- go.opentelemetry.io/otel/bridge/opencensus
|
||||||
|
@ -29,12 +29,12 @@ module-sets:
|
||||||
- go.opentelemetry.io/otel/sdk/metric
|
- go.opentelemetry.io/otel/sdk/metric
|
||||||
- go.opentelemetry.io/otel/trace
|
- go.opentelemetry.io/otel/trace
|
||||||
experimental-metrics:
|
experimental-metrics:
|
||||||
version: v0.52.0
|
version: v0.51.0
|
||||||
modules:
|
modules:
|
||||||
- go.opentelemetry.io/otel/example/prometheus
|
- go.opentelemetry.io/otel/example/prometheus
|
||||||
- go.opentelemetry.io/otel/exporters/prometheus
|
- go.opentelemetry.io/otel/exporters/prometheus
|
||||||
experimental-logs:
|
experimental-logs:
|
||||||
version: v0.6.0
|
version: v0.5.0
|
||||||
modules:
|
modules:
|
||||||
- go.opentelemetry.io/otel/log
|
- go.opentelemetry.io/otel/log
|
||||||
- go.opentelemetry.io/otel/sdk/log
|
- go.opentelemetry.io/otel/sdk/log
|
||||||
|
@ -42,7 +42,7 @@ module-sets:
|
||||||
- go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp
|
- go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp
|
||||||
- go.opentelemetry.io/otel/exporters/stdout/stdoutlog
|
- go.opentelemetry.io/otel/exporters/stdout/stdoutlog
|
||||||
experimental-schema:
|
experimental-schema:
|
||||||
version: v0.0.9
|
version: v0.0.8
|
||||||
modules:
|
modules:
|
||||||
- go.opentelemetry.io/otel/schema
|
- go.opentelemetry.io/otel/schema
|
||||||
excluded-modules:
|
excluded-modules:
|
||||||
|
|
|
@ -561,7 +561,7 @@ github.com/pkg/errors
|
||||||
# github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
|
# github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
|
||||||
## explicit
|
## explicit
|
||||||
github.com/pmezard/go-difflib/difflib
|
github.com/pmezard/go-difflib/difflib
|
||||||
# github.com/prometheus/client_golang v1.20.3
|
# github.com/prometheus/client_golang v1.20.4
|
||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil
|
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil
|
||||||
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header
|
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header
|
||||||
|
@ -979,8 +979,8 @@ go.mongodb.org/mongo-driver/bson/bsonrw
|
||||||
go.mongodb.org/mongo-driver/bson/bsontype
|
go.mongodb.org/mongo-driver/bson/bsontype
|
||||||
go.mongodb.org/mongo-driver/bson/primitive
|
go.mongodb.org/mongo-driver/bson/primitive
|
||||||
go.mongodb.org/mongo-driver/x/bsonx/bsoncore
|
go.mongodb.org/mongo-driver/x/bsonx/bsoncore
|
||||||
# go.opentelemetry.io/otel v1.30.0
|
# go.opentelemetry.io/otel v1.29.0
|
||||||
## explicit; go 1.22
|
## explicit; go 1.21
|
||||||
go.opentelemetry.io/otel
|
go.opentelemetry.io/otel
|
||||||
go.opentelemetry.io/otel/attribute
|
go.opentelemetry.io/otel/attribute
|
||||||
go.opentelemetry.io/otel/baggage
|
go.opentelemetry.io/otel/baggage
|
||||||
|
@ -999,50 +999,50 @@ go.opentelemetry.io/otel/semconv/v1.20.0/httpconv
|
||||||
go.opentelemetry.io/otel/semconv/v1.24.0
|
go.opentelemetry.io/otel/semconv/v1.24.0
|
||||||
go.opentelemetry.io/otel/semconv/v1.26.0
|
go.opentelemetry.io/otel/semconv/v1.26.0
|
||||||
go.opentelemetry.io/otel/semconv/v1.7.0
|
go.opentelemetry.io/otel/semconv/v1.7.0
|
||||||
# go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0
|
# go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0
|
||||||
## explicit; go 1.22
|
## explicit; go 1.21
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform
|
||||||
# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0
|
# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0
|
||||||
## explicit; go 1.22
|
## explicit; go 1.21
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/envconfig
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/envconfig
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/retry
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/retry
|
||||||
# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0
|
# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0
|
||||||
## explicit; go 1.22
|
## explicit; go 1.21
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/envconfig
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/envconfig
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/retry
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/retry
|
||||||
# go.opentelemetry.io/otel/exporters/prometheus v0.52.0
|
# go.opentelemetry.io/otel/exporters/prometheus v0.51.0
|
||||||
## explicit; go 1.22
|
## explicit; go 1.21
|
||||||
go.opentelemetry.io/otel/exporters/prometheus
|
go.opentelemetry.io/otel/exporters/prometheus
|
||||||
# go.opentelemetry.io/otel/metric v1.30.0
|
# go.opentelemetry.io/otel/metric v1.29.0
|
||||||
## explicit; go 1.22
|
## explicit; go 1.21
|
||||||
go.opentelemetry.io/otel/metric
|
go.opentelemetry.io/otel/metric
|
||||||
go.opentelemetry.io/otel/metric/embedded
|
go.opentelemetry.io/otel/metric/embedded
|
||||||
go.opentelemetry.io/otel/metric/noop
|
go.opentelemetry.io/otel/metric/noop
|
||||||
# go.opentelemetry.io/otel/sdk v1.30.0
|
# go.opentelemetry.io/otel/sdk v1.29.0
|
||||||
## explicit; go 1.22
|
## explicit; go 1.21
|
||||||
go.opentelemetry.io/otel/sdk
|
go.opentelemetry.io/otel/sdk
|
||||||
go.opentelemetry.io/otel/sdk/instrumentation
|
go.opentelemetry.io/otel/sdk/instrumentation
|
||||||
go.opentelemetry.io/otel/sdk/internal/env
|
go.opentelemetry.io/otel/sdk/internal/env
|
||||||
go.opentelemetry.io/otel/sdk/internal/x
|
go.opentelemetry.io/otel/sdk/internal/x
|
||||||
go.opentelemetry.io/otel/sdk/resource
|
go.opentelemetry.io/otel/sdk/resource
|
||||||
go.opentelemetry.io/otel/sdk/trace
|
go.opentelemetry.io/otel/sdk/trace
|
||||||
# go.opentelemetry.io/otel/sdk/metric v1.30.0
|
# go.opentelemetry.io/otel/sdk/metric v1.29.0
|
||||||
## explicit; go 1.22
|
## explicit; go 1.21
|
||||||
go.opentelemetry.io/otel/sdk/metric
|
go.opentelemetry.io/otel/sdk/metric
|
||||||
go.opentelemetry.io/otel/sdk/metric/internal
|
go.opentelemetry.io/otel/sdk/metric/internal
|
||||||
go.opentelemetry.io/otel/sdk/metric/internal/aggregate
|
go.opentelemetry.io/otel/sdk/metric/internal/aggregate
|
||||||
go.opentelemetry.io/otel/sdk/metric/internal/exemplar
|
go.opentelemetry.io/otel/sdk/metric/internal/exemplar
|
||||||
go.opentelemetry.io/otel/sdk/metric/internal/x
|
go.opentelemetry.io/otel/sdk/metric/internal/x
|
||||||
go.opentelemetry.io/otel/sdk/metric/metricdata
|
go.opentelemetry.io/otel/sdk/metric/metricdata
|
||||||
# go.opentelemetry.io/otel/trace v1.30.0
|
# go.opentelemetry.io/otel/trace v1.29.0
|
||||||
## explicit; go 1.22
|
## explicit; go 1.21
|
||||||
go.opentelemetry.io/otel/trace
|
go.opentelemetry.io/otel/trace
|
||||||
go.opentelemetry.io/otel/trace/embedded
|
go.opentelemetry.io/otel/trace/embedded
|
||||||
go.opentelemetry.io/otel/trace/noop
|
go.opentelemetry.io/otel/trace/noop
|
||||||
|
|
|
@ -143,7 +143,7 @@ function InstanceSettingsForm({ data: instance }: InstanceSettingsFormProps) {
|
||||||
<TextArea
|
<TextArea
|
||||||
field={form.shortDesc}
|
field={form.shortDesc}
|
||||||
label={`Short description (markdown accepted, max ${shortDescLimit} characters)`}
|
label={`Short description (markdown accepted, max ${shortDescLimit} characters)`}
|
||||||
placeholder="A small testing instance for the GoToSocial alpha software."
|
placeholder="A small testing instance for GoToSocial."
|
||||||
autoCapitalize="sentences"
|
autoCapitalize="sentences"
|
||||||
rows={6}
|
rows={6}
|
||||||
/>
|
/>
|
||||||
|
@ -151,7 +151,7 @@ function InstanceSettingsForm({ data: instance }: InstanceSettingsFormProps) {
|
||||||
<TextArea
|
<TextArea
|
||||||
field={form.description}
|
field={form.description}
|
||||||
label={`Full description (markdown accepted, max ${descLimit} characters)`}
|
label={`Full description (markdown accepted, max ${descLimit} characters)`}
|
||||||
placeholder="A small testing instance for the GoToSocial alpha software. Just trying it out, my main instance is https://example.com"
|
placeholder="A small testing instance for GoToSocial. Just trying it out, my main instance is https://example.com"
|
||||||
autoCapitalize="sentences"
|
autoCapitalize="sentences"
|
||||||
rows={6}
|
rows={6}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -52,10 +52,10 @@ export default function UserRouter() {
|
||||||
<Route path="/emailpassword" component={EmailPassword} />
|
<Route path="/emailpassword" component={EmailPassword} />
|
||||||
<Route path="/migration" component={UserMigration} />
|
<Route path="/migration" component={UserMigration} />
|
||||||
<Route path="/export-import" component={ExportImport} />
|
<Route path="/export-import" component={ExportImport} />
|
||||||
|
<InteractionRequestsRouter />
|
||||||
<Route><Redirect to="/profile" /></Route>
|
<Route><Redirect to="/profile" /></Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<InteractionRequestsRouter />
|
|
||||||
</Router>
|
</Router>
|
||||||
</BaseUrlContext.Provider>
|
</BaseUrlContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -73,13 +73,11 @@ function InteractionRequestsRouter() {
|
||||||
return (
|
return (
|
||||||
<BaseUrlContext.Provider value={absBase}>
|
<BaseUrlContext.Provider value={absBase}>
|
||||||
<Router base={thisBase}>
|
<Router base={thisBase}>
|
||||||
<ErrorBoundary>
|
<Switch>
|
||||||
<Switch>
|
<Route path="/search" component={InteractionRequests} />
|
||||||
<Route path="/search" component={InteractionRequests} />
|
<Route path="/:reqId" component={InteractionRequestDetail} />
|
||||||
<Route path="/:reqId" component={InteractionRequestDetail} />
|
<Route><Redirect to="/search"/></Route>
|
||||||
<Route><Redirect to="/search"/></Route>
|
</Switch>
|
||||||
</Switch>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</Router>
|
</Router>
|
||||||
</BaseUrlContext.Provider>
|
</BaseUrlContext.Provider>
|
||||||
);
|
);
|
||||||
|
|