A service which grabs nostr notes and posts them to Twitter as tweets. Right now it only supports notes that aren't replies. The user opens the website, logs in with their Twitter account and sets a list of npubs from which notes will be cross-posted to their Twitter account.
The idea behind the service is that it is pretty simple and easy to use. Because of this we just ask the users to paste their npub in and ignore the matter of their relays completely. Instead, we try to grab their relays from Purple Pages in the background. As it stands now it would seem that we can't grab relay lists for ~50% of feeds. This is a problem that will hopefully be rectified in the future in some way, perhaps by creating a service similar to Purple Pages that crawls relays more aggressively.
Posting tweets via the Twitter API seems to be failing often. We mostly get two kinds of errors:
- rate limit exceeded
- unauthorized
I am not sure where the first one comes from since we are not exceeding rate limits but perhaps it is somehow related to personal per-account limits of Twitter accounts we are trying to post to. Unfortunately the API documentation isn't helpful and doesn't explain this. The second one probably comes from users revoking access for the app used by this service in their account settings.
If a tweet is stuck in the queue and can't be posted for a certain amount of time we give up on posting it after several days and simply drop it completely. The reasoning is that suddenly seeing tweets for notes that are quite old would be confusing and probably not desired. Additionally there is almost no chance that we will ever manage to post those tweets based on the metrics I am seeing.
In order to handle Twitter API errors tweets are scheduled to be sent by publishing them to an internal queue. Think of this in terms of a command bus.
flowchart TB
process-received-event-handler["ProcessReceivedEventHandler"]
send-tweet-handler["SendTweetHandler"]
tweet-created-event-subscriber["TweetCreatedEventSubscriber"]
process-received-event-handler --> |pubsub event| tweet-created-event-subscriber
tweet-created-event-subscriber --> |command| send-tweet-handler
Build the program like so:
$ go build -o crossposting-service ./cmd/crossposting-service
$ ./crossposting-service
The program takes no arguments. There is a Dockerfile available.
Configuration is performed using environment variables. This is also the case for the Dockerfile.
Listen address for the main webserver in the format accepted by the Go standard library.
Optional, defaults to :8008
if empty.
Listen address for the prometheus metrics server in the format accepted by the
Go standard library. The metrics are exposed under path /metrics
.
Optional, defaults to :8009
if empty.
Execution environment. Setting environment to DEVELOPMENT
:
- replaces a Twitter API adapter with a fake adapter
- it doesn't actually post to Twitter
- it returns hardcoded fake Twitter account details (due to weird rate-limiting errors)
Optional, can be set to PRODUCTION
or DEVELOPMENT
. Defaults to PRODUCTION
.
Log level.
Optional, can be set to TRACE
, DEBUG
, ERROR
or DISABLED
. Defaults to
DEBUG
.
Twitter API consumer key.
Required.
Twitter API consumer key secret.
Required.
Full path to the database file.
Required, e.g. /some/directory/database.sqlite
.
Public facing address of the service, required for Twitter callbacks.
Required, e.g. http://localhost:8008/
or https://example.com/
.
The keys you are after are "Consumer keys". See "How to get access to the Twitter API".
Requirements:
- Your app must be a part of the project, it can't be standalone.
- App permissions need to be set to "Read and write".
- Type of app needs to be set to "Web App, Automated App or Bot".
- You need to set "Callback URI" accordingly:
- for local development to e.g.
http://localhost:8008/login-callback
(unless you setCROSSPOSTING_ENVIRONMENT
toDEVELOPMENT
deactivating interacting with the Twitter API) - in production this has to be e.g.
https://example.com/login-callback
- for local development to e.g.
See configuration for the address of our metrics endpoint. Many out-of-the-box Go-related metrics are available. We also have custom metrics:
application_handler_calls_total
application_handler_calls_duration
subscription_queue_length
version
public_key_downloader_count
public_key_downloader_relays_count
relay_connection_state
twitter_api_calls
accounts_count
linked_public_keys_count
See service/adapters/prometheus
.
We deploy this service using ansible. The steps for deployment can be read in our internal notion page.
The project usually uses the latest Go version as declared by the go.mod
file.
You may not be able to build it using older compilers.
Run the following command changing appropriate environment variables:
CROSSPOSTING_TWITTER_KEY=xxx \
CROSSPOSTING_TWITTER_KEY_SECRET=xxx \
CROSSPOSTING_DATABASE_PATH=/path/to/database.sqlite \
CROSSPOSTING_ENVIRONMENT=DEVELOPMENT \
CROSSPOSTING_PUBLIC_FACING_ADDRESS=http://localhost:8008/ \
go run ./cmd/crossposting-service
Frontend is written in Vue and located in ./frontend
. Precompiled files are
supposed to be commited as they are embedded in executable files.
In order to update the embedded compiled frontend files run the following command:
$ make frontend
We recommend reading the Makefile
to discover some targets which you can
execute. It can be used as a shortcut to run various useful commands.
You may have to run the following command to install a linter and a code formatter before executing certain targets:
$ make tools
If you want to check if the pipeline will pass for your commit it should be enough to run the following command:
$ make ci
It is also useful to often run just the tests during development:
$ make test
Easily format your code with the following command:
$ make fmt
Resources which are in my opinion informative and good to read:
When naming tests which tests a specific behaviour it is recommended to follow a
pattern TestNameOfType_ExpectedBehaviour
. Example:
TestRelayDownloader_EventsDownloadedFromRelaysArePublishedUsingPublisher
.
Some constructors are prefixed with the word Must
. Those constructors panic
and should always be accompanied by a normal constructor which isn't prefixed
with the Must
and returns an error. The panicking constructors should only be
used in the following cases:
- when writing tests
- when a static value has to be created e.g.
MustNewHops(1)
and this branch of logic in the code is covered by tests