Experimental browser for the Atmosphere
{ "uri": "at://did:plc:oisofpd7lj26yvgiivf3lxsi/com.whtwnd.blog.entry/3leqp35gald2o", "cid": "bafyreidszlg2e6jowcgmrubt3xefz6g6nmctonhyhpo33ivxg5fzrbrl2a", "value": { "ogp": { "url": "https://haileyok.com/blog-images/bladerunner.jpeg", "width": 1200, "height": 550 }, "$type": "com.whtwnd.blog.entry", "theme": "github-light", "title": "Working with Automod as a Labeler (Draft)", "content": "Since we initially rolled out Ozone to third parties, there have been a number of labelers pop up across the network. From the game labelers like [TTRPG](https://bsky.app/profile/bskyttrpg.bsky.social) to utilities like the [Pronouns](https://bsky.app/profile/pronouns.adorable.mom) labeler to true moderation labelers such as [Laelaps](https://bsky.app/profile/did:plc:lcdcygpdeiittdmdeddxwt4w) or [Asuka Field](https://bsky.app/profile/asukafield.xyz). TTRPG and Pronouns both operate automatically. The TTRPG labeler assigns labels at random that list the given user’s class, and the Pronouns labeler “reacts” to likes on the network, assigning the user’s selected pronouns once they like a post. For the most part, these two labelers have a pretty easy time \\- at least in terms of “moderation” work.\n\nOn the other hand, Laelaps and Asuka Field are much more hands on. Users report posts or accounts within the app to either of these two labelers, and moderators at either of the two labelers respond by actioning and/or acknowledging the incoming reports. At a small scale, this works pretty well. Check in every so often, handle a few reports. However, once you start scaling \\- and some of these labelers definitely have\\! \\- things become much more time consuming. The official Bluesky moderation service has a similar problem, and as a result, one piece of the puzzle that we created as our automod system (called Hepa, like the air filters). It helps us take action on posts and accounts automatically for a whole slew of reasons, but it is also quite powerful. Wouldn’t it be nice if third parties could benefit as well?\n\nIn this post, we’ll explore three different ways that automod rules can be implemented using Hepa, all with \\- as we can see from these active lablers \\- real world applications.\n\n## Before we start\n\nOne thing to note before we begin is that as of right now, you need to write some Go to implement rules. The skill level required is quite low for most application (in fact, the first time I ever really wrote Go was writing automod rules, and now I’m in love with the language) and following simple patterns makes a lot of code re-usable. In the future, we would like to have a more robust system, possibly either in a much simpler scripting language or even some sort of no-code system to implement rules.\n\nI’m going to assume a few things:\n\n- You either know Go or are comfortable learning \n- You know to “nil check” pointers in Go (if you didn’t now you do) \n- You’re already running an [Ozone](https://github.com/bluesky-social/ozone) instance \n- You are comfortable doing a small bit of sysadmin work (again, if you’re running Ozone I think this is a safe assumption\\!) \n - You have redis-server installed\n\nLet’s also get a little bit of configuration out of the way. We’ll clone the repo and set some basic settings in the environment for our automod instance.\n\n```\ngit clone git@github.com:bluesky-social/indigo.git\ncd indigo\ngit checkout hailey/self-hosted-ozone-automod # For now, until I merge\n```\n\nCreate a `.env` file, and add the following:\n\n```\nHEPA_MODE=\"labeler\" # This allows us to use regular authentication\nHEPA_FIREHOSE_PARALLELISM=600 # Arbitrary number. Shouldn't be too low, but probably doesn't need to be this high either.\nHEPA_REDIS_URL=\"redis://localhost:6379/0\"\nHEPA_PLC_RATE_LIMIT=800\nHEPA_QUOTA_MOD_ACTION_DAY=5000\n\nATP_OZONE_HOST=\"https://ozone.haileyok.com\" # Host of the ozone instance\nHEPA_OZONE_SERVICE_DID=\"did:plc:abc123\" # This is the did of the labeler account itself. \n\nHEPA_OZONE_DID=\"did:plc:123456\" # This is the did of the moderator account\nHEPA_OZONE_MOD_PASS=\"\" # The password for the moderator account\nHEPA_OZONE_MOD_SERVICE=\"https://inkcap.us-east.host.bsky.network\" # The PDS url the moderator account is on. Do not simply use bsky.social, use the actual PDS host\n\nATP_BSKY_HOST=\"https://api.bsky.app\" # You probably don't want to change this\nATP_BGS_HOST=\"wss://bsky.network\" # You probably want this too. If you're already subscribing to the firehose for other services, you can use Rainbow!\n\n\nHEPA_LOG_LEVEL=warn\nHEPA_METRICS_LISTEN=\":3989\" # Optional prometheus metrics for observability\n```\n\nFinally, remove all of the rules that are active in `automod/rules/all.go`. All of the rules should be either commented out or removed entirely from the various arrays in this file before attempting to run Hepa.\n\nOnce you have removed those rules, try running Automod with `go run ./cmd/hepa run`. You shouldn’t get any errors, and you’ll probably have an empty console (Note: you may encounter errors with looking up profiles. This is a known “bug” right now, but simply means the profile for an event \n\n### Pronouns Bot\n\nWe’ll start with the pronouns example. While Hepa might be a bit hefty for something like assigning pronouns to users, it is a great example to illustrate. We are going to create a “record rule” for this, since there isn’t a “like rule” implemented.\n\nWe’ll begin by creating a post that we want to track the likes on and get the URI of that post. Then, we can write the rule\\!\n\n```\npackage rules\n\nimport \"github.com/bluesky-social/indigo/automod\"\n\n// We'll create a map of all the different URIs that we're looking for\nvar pronounsPostMap = map[string]string{\n\t\"at://did:plc:123/app.bsky.feed.post/1234\": \"she-her\",\n}\n\n// Record rules take in a record context and return an error\nfunc PronounsLikeRule(c *automod.RecordContext) error {\n\t// `Account` contains various pieces of metadata that are hydrated for you. If the identity is `nil`, then soemthing went wrong while hydrating that, so we'll skip the rule\n\tif c.Account.Identity == nil {\n \t\treturn nil\n\t}\n\n\t// We don't care about this event if it isn't a like event. So, we'll skip.\n\tif c.RecordOp.Collection != \"app.bsky.feed.like\" {\n \t\treturn nil\n\t}\n\n\t// If the url isn't inside of our post map, we also don't care so we'll skip\n\tlabel, ok := pronounsPostMap[c.RecordOp.ATURI().String()]\n\tif !ok {\n \t\treturn nil\n\t}\n\n\t// If the action is a \"create\" action, then we'll apply the label\n\tif c.RecordOp.Action == \"create\" {\n \t\tc.AddAccountLabel(label)\n\t}\n\n\t// We don't have a RemoveAccountLabel...heh\n\n\treturn nil\n}\n\n\n```\n\nSee? That was super easy\\! There isn’t much to go over here, but we can touch on what exactly is available inside of `Account`. `Account` is hydrated whenever an event is received, and by the time your rule executes it will be available to you. The metadata gets cached in Redis for some time so as to not constantly re-fetch data that already exists.\n\nProfile metadata such as follow counts, display name, bio description, or avatar and banner CIDs are all inside of `Account`. While they are not too useful for this rule, they can play a vital role in determining whether or not to action an account as we’ll see in future rules.\n\nNote: Any time that you encounter a pointer, you want to check for `nil` before trying to access the value. For example, the `DisplayName` field is a pointer, because it may not be set by the user (this is a weird quirk of Golang). To do so, you might write\n\n```\nif c.Account.Profile.DisplayName != nil && *c.Account.Profile.DisplayName == \"hailey\" {\n \tc.AddAccountLabel(\"hailey\")\n}\n```\n\nHepa will graceful handle null pointers, however your rules may fail to execute properly as a result so you should always pay close attention. If null pointers occur, they will be logged by Hepa.\n\nTo test our rule, let’s update `all.go` with the new rule we just created. Your `all.go` may look like this:\n\n```\npackage rules\n\nimport (\n\t\"github.com/bluesky-social/indigo/automod\"\n)\n\n// IMPORTANT: reminder that these are the indigo-edition rules, not production rules\nfunc DefaultRules() automod.RuleSet {\n\trules := automod.RuleSet{\n \tPostRules:\t[]automod.PostRuleFunc{},\n \tProfileRules: []automod.ProfileRuleFunc{},\n \tRecordRules: []automod.RecordRuleFunc{\n \tPronounsLikeRule,\n \t},\n \tRecordDeleteRules: []automod.RecordRuleFunc{},\n \tIdentityRules: \t[]automod.IdentityRuleFunc{},\n \tBlobRules: \t[]automod.BlobRuleFunc{},\n \tNotificationRules: []automod.NotificationRuleFunc{},\n \tOzoneEventRules: []automod.OzoneEventRuleFunc{},\n\t}\n\treturn rules\n}\n```\n\nNow all you need to do is run `go run ./cmd/hepa run`, like the post, and watch your labeler apply the label when you like it.", "createdAt": "2025-01-02T08:55:52.600Z", "visibility": "public" } }