diff --git a/README.md b/README.md
index 41fc07f..b28ae21 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,3 @@
# misskey-plugins
Collection of plugins for Misskey and associated software.
-
-
----
-## Getting Started
-
-Welcome to your new KitsuDev Repo, bunnybeam! Feel free to make this place your own!
-
-## Make your first push!
-Once you're ready to push your files, use these commands to get to your spot!
-```bash
-git remote add origin https://kitsunes.dev/bunnybeam/misskey-plugins.git
-git branch -M main
-git push -uf origin main
-```
-
-Once you're all set, please [Make a README](https://www.makeareadme.com/)!
-
----
-Have fun, and happy coding!
-
--- Kio
-
-(p.s. and while you're at it, say hi to pearl.)
diff --git a/arr-equal.ais b/arr-equal.ais
new file mode 100644
index 0000000..0df6206
--- /dev/null
+++ b/arr-equal.ais
@@ -0,0 +1,51 @@
+@arr_equal(arr1, arr2) {
+ if arr1.len != arr2.len {
+ return false
+ }
+ for let i,arr1.len {
+ if arr1[i] != arr2[i] {
+ return false
+ }
+ }
+ return true
+}
+
+// TESTS
+
+let unit_tests = []
+
+@deftest(test_name, test_func) {
+ unit_tests.push({name: test_name, func: test_func})
+}
+
+deftest("arr_equal pos", @() {
+ return arr_equal([1, 2, 3], [1, 2, 3])
+})
+
+deftest("arr_equal neg element", @() {
+ return !arr_equal([1, 2, 3], [1, 2, 4])
+})
+
+deftest("arr_equal neg size", @() {
+ return !arr_equal([1, 2, 3], [1, 2])
+})
+
+@run_all_tests() {
+ print("Running unit tests...")
+ var fails = 0
+ each let test, unit_tests {
+ if !test.func() {
+ fails += 1
+ print(`Test {test.name} failed.`)
+ }
+ }
+ if fails == 0 {
+ print(`All {unit_tests.len} tests successful!`)
+ return true
+ }
+ print(`{fails}/{unit_tests.len} tests failed.`)
+ Mk:dialog("Load error", "An error occurred while loading the plugin. Please contact the script author.", "error")
+ return false
+}
+
+run_all_tests()
diff --git a/headmate-hopper.ais b/headmate-hopper.ais
new file mode 100644
index 0000000..65fb627
--- /dev/null
+++ b/headmate-hopper.ais
@@ -0,0 +1,174 @@
+/// @ 0.19.0
+### {
+ name: "Headmate Hopper"
+ version: "1.0.0"
+ author: "@bunnybeam@transfem.social"
+ description: "Adds a tag containing the name of the selected headmate to the end of all new posts."
+ permissions: ["read:account", "write:account"]
+ config: {
+ headmates: {
+ type: "string"
+ label: "Headmates"
+ description: "List of headmate names. Separate with commas."
+ default: "Headmate 1, Headmate 2"
+ }
+ change_name: {
+ type: "boolean"
+ label: "Change display name"
+ description: "If enabled, selecting a headmate will add them to your display name. Requires permission to view and edit account info."
+ default: false
+ }
+ enable_attribution: {
+ type: "boolean"
+ label: "Enable attribution"
+ description: "If enabled, an attribution tag will be inserted into the post, linking back to the plugin page."
+ default: false
+ }
+ tag_template: {
+ type: "string"
+ label: "Tag template"
+ description: "Template for the headmate tag. '%[name]' is replaced with the current headmate name."
+ default: "- posted by %[name]"
+ }
+ note_template: {
+ type: "string"
+ label: "Note template"
+ description: "Template for new notes. %[text] - Raw note contents. %[br] - Line break. %[tag] - Headmate tag. %[attrib] - Attribution tag (or nothing if attribution is disabled)."
+ default: "%[text]%[br]%[br]%[tag]%[attrib]"
+ }
+ attribution_tag: {
+ type: "string"
+ label: "Attribution tag"
+ description: "Content of the attribution tag. Inserted when attribution is enabled. '%[br]' is replaced with a line break."
+ default: "%[br]?[Headmate Hopper](https://kitsunes.dev/bunnybeam/misskey-plugins/src/branch/main/headmate-hopper.ais)"
+ }
+ }
+}
+
+// Format the note, inserting the note text, headmate tag and attribution tag into the template
+@format_note(template, note_text, headmate_tag, attribution_tag) {
+ if Plugin:config.enable_attribution {
+ template = template.replace("%[attrib]", attribution_tag)
+ } else {
+ template = template.replace("%[attrib]", "")
+ }
+ template = template.replace("%[br]", Str:lf)
+ template = template.replace("%[tag]", headmate_tag)
+ template = template.replace("%[text]", note_text)
+ return template
+}
+
+// Return the tag template with the headmate name inserted
+@make_tag_from_name(headmate_name) {
+ return Plugin:config.tag_template.replace("%[name]", headmate_name)
+}
+
+// Get the list of headmates by splitting the config line and trimming the results
+@get_headmate_list() {
+ let raw_list = Plugin:config.headmates.split(",")
+ let new_list = []
+ each let name, raw_list {
+ new_list.push(name.trim())
+ }
+ return new_list
+}
+
+// Check if `text` already contains the headmate tag
+@already_tagged(text) {
+ return text.incl(make_tag_from_name(headmate_name))
+}
+
+// Attempt to change the display name. Returns true if successful, false otherwise.
+@change_display_name(name) {
+ // Attempt to read the display name
+ let read_res = Mk:api("i", {})
+ if read_res.info != null {
+ var message = "An unknown error occurred when attempting to read the current display name."
+ if read_res.info.code == "PERMISSION_DENIED" {
+ message = "The 'View account information' permission must be enabled in order to change your display name."
+ }
+ Mk:dialog("Error reading display name", message, "error")
+ return false
+ }
+ var new_name = read_res.name.replace(` ({headmate_name})`, "")
+ if name != "" {
+ new_name = `{new_name} ({name})`
+ }
+
+ // Attempt to set the display name
+ let write_res = Mk:api("i/update", {name: new_name})
+ if write_res.info != null {
+ var message = "An unknown error occurred when attempting to edit the current display name."
+ if write_res.info.code == "PERMISSION_DENIED" {
+ message = "The 'Edit account information' permission must be enabled in order to change your display name."
+ }
+ Mk:dialog("Error setting display name", message, "error")
+ return false
+ }
+ return true
+}
+
+// Select a new headmate name
+@select_headmate_name(name) {
+ if Plugin:config.change_name {
+ if !change_display_name(name) {
+ return false
+ }
+ }
+ headmate_name = name
+ Mk:save("headmate", name)
+}
+
+@arr_equal(arr1, arr2) {
+ if arr1.len != arr2.len {
+ return false
+ }
+ for let i,arr1.len {
+ if arr1[i] != arr2[i] {
+ return false
+ }
+ }
+ return true
+}
+
+Plugin:register_note_post_interruptor(@(note) {
+ // Filter to only the fields we're allowed to use
+ let new_note = {}
+ let keys = Obj:keys(note)
+ each let key, keys {
+ if note[key] != null {
+ new_note[key] = note[key]
+ }
+ }
+
+ // Skip if there is no headmate selected
+ if headmate_name == "" {
+ return new_note
+ }
+
+ // Skip if the note already contains the tag
+ let headmate_tag = make_tag_from_name(headmate_name)
+ if new_note.text.incl(headmate_tag) {
+ return new_note
+ }
+
+ new_note.text = format_note(Plugin:config.note_template, new_note.text, headmate_tag, Plugin:config.attribution_tag)
+
+ return new_note
+})
+
+// Register post form actions for each headmate
+each let name, get_headmate_list() {
+ Plugin:register_post_form_action(`Switch to {name}`, @(note, rewrite) {
+ select_headmate_name(name)
+ })
+}
+Plugin:register_post_form_action("Clear fronter", @(note, rewrite) {
+ select_headmate_name("")
+})
+
+// Load the persistent selected headmate
+var headmate_name = Mk:load("headmate")
+if headmate_name == null {
+ select_headmate_name("")
+}