Home Lister for Firefish
Post
Cancel

Lister for Firefish

Convenience is word for this Firefish plugin. See a post from someone interesting, open the action item menu, add them to one of your 3 favorite lists in just two clicks. Syndication

Listen, do you have lists of lists? Listless for efficiency? The Lister Firefish plugin is made for you. Quickly add users to a list right from a note in just two clicks and improve your efficiency quotas by…some large boring amount. Or just try it for fun.

Overview

Firefish has a lists feature, similar to Mastodon. By organizing interesting accounts into lists, you can create focused timelines of interesting users. Gather all your fellow #Knitters into a list, or really hone in on a specific topic like #PigsOfMastodon (this is real, not what you think it is, and sometimes adorable, y’all). However, unlike Mastodon, you don’t have to be following someone to add them to a list and they won’t show up in your main timeline.

This Firefish Plugin reduces the amount of clicks it takes to add someone to a list. Previously, if you found someone interesting, you would have to click through to their profile, open the action item menu, choose “Add to List”, and then select the list you wanted to add them to, then click OK. This plugin adds a button to the notes action item menu that allows you to add them to a list directly from the timeline, all with two clicks (Click to menu item, click to add to list).

If you’re a developer and interested in learning about some of the specific AiScript functions used in this plugin, check out the end of this post, where I go into a deep dive.

Add to List Plugin

Firefish Plugins are a great way to customize your experience on the platform.

I created this plugin, along with several others to gain an understanding of how AiScript works, and Firefish Plugins in general.

Those interested in learning AiScript may find it useful as a learning tool. Take a look at the code and see how it works!

New to plugins? Check out my earlier post here, which goes into detail about what plugins are and how to install them.

Settings

The plugin has a few settings you can configure to customize your experience.

You can add up to 3 lists to the plugin. The lists are displayed in the order you add them to the plugin. You don’t HAVE to add 3, that’s just the maximum. Leave the rest blank and they won’t show up.

This is the most tricky part. You’re going to have to do some sleuthing to find the list IDs. I’ll try to make it as easy as possible.

  1. Open the list you want to add to the plugin. (More > Lists > Select Your List) You’ll end up at something like https://fedia.social/my/lists/32ks911k
  2. Look at the URL. It should look something like this: https://fedia.social/my/lists/1234567890abcdef1234567890abcdef
  3. The list ID is the last part of the URL. In this case, it’s 1234567890abcdef1234567890abcdef
  4. Copy the list ID and paste it into the plugin settings, along with the name of the list or a shorter abbreviation.
  5. Repeat for each list you want to add.

Once you’ve got all that setup, you’re good to go!

Setting Description
Debug Mode For developers, shows messages in the console. Off by default.
List 1 Id The list id you want included in the notes action items.
List 1 Name A short name for your list that will be visible in the notes action items.
List 2 Id The list id you want included in the notes action items.
List 2 Name A short name for your list that will be visible in the notes action items.
List 3 Id The list id you want included in the notes action items.
List 3 Name A short name for your list that will be visible in the notes action items.

Permissions

This plugin is going to prompt you to accept the following permissions. These permissions are required for the plugin to work.

I can’t urge you enough to read the permissions and understand what they do. If you’re not comfortable with the permissions, don’t install the plugin. Even this one.

Permission Description
Read:Account This permission is required to retrieve the users existing in a list
Write:Account This permission is required to add users to a list

Installation

On your Firefish server, you can install this plugin by copying the code below into the Settings > Plugins section. You can read more about installing plugins here or watch a video demonstration of the process here.

Hope you find it useful! More plugins coming soon. Check out the previously introduced Highligter plugin and the Share to Mastodon plugin.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/// @ 0.11.1
### {
  name: "Lister"
  version: "0.11.1.0.001"
  author: "@box464@firefish.social"
  description: "Adds a new action item to notes that allows you to add the account to a specific list. Find the Ids of your lists by going to: https://your.Firefish /my/lists, click on list, and the Id will be the last bit in the url (i.e. https://fedia.social/my/lists/9e323h6hwx, you'll copy 9e323h6hwx for the List Id)"
  permissions: ["write:account", "read:account"]
  // Write:Account is required to add users to a list, see https://fedia.social/api-doc#operation/users/lists/push
  // Read:Account is required to retrieve the users existing in a list, see https://fedia.social/api-doc#operation/users/lists/show
  config: {
     debug: {
      type: "boolean"
      label: "Enable Debugging"
      description: "Write debugging information to the console."
      default: no
    }
    list01Id: {
      type: "string"
      label: "List 1: Id"
      description: "Required. The Id of your associated list."
      default: ""
    }
    list01Name: {
      type: "string"
      label: "List 1: Short Name"
      description: "An abbreviated version of the list name that will appear as the menu action item."
      default: ""
    }
    list02Id: {
      type: "string"
      label: "List 2: Id"
      description: "The Id of your associated list."
      default: ""
    }
    list02Name: {
      type: "string"
      label: "List 2: Short Name"
      description: "An abbreviated version of the list name that will appear as the menu action item."
      default: ""
    }
    list03Id: {
      type: "string"
      label: "List 3: Id"
      description: "The Id of your associated list."
      default: ""
    }
    list03Name: {
      type: "string"
      label: "List 3: Short Name"
      description: "An abbreviated version of the list name that will appear as the menu action item."
      default: ""
    }
  }
}

@debug(comment) {
  ? (Plugin:config.debug = yes) {
    print("Plugin: Add to List")
    print(comment)
  }
}

// Create an object to hold the configuration variables
#c = {}
    c.list01Id <- Plugin:config.list01Id
    c.list01Name <- Plugin:config.list01Name
    c.list02Id <- Plugin:config.list02Id
    c.list02Name <- Plugin:config.list02Name
    c.list03Id <- Plugin:config.list03Id
    c.list03Name <- Plugin:config.list03Name
    c.debug <- Plugin:config.debug


@formatListName(listName, listNumber) {
  $listName_ <- listName
  $listNumber_ <- listNumber

  ? ((listName_ = _) | (Str:len(listName_) = 0)) {
      listName_ <- Arr:join(["Add to List " listNumber_])
  } . {
    listName_ <- Arr:join(["Add to List: " listName_])
  }

 listName_

}

c.list01Name <- formatListName(c.list01Name, "01")
c.list02Name <- formatListName(c.list02Name, "02")
c.list03Name <- formatListName(c.list03Name, "03")

@effect(note, listName, listId) {
  #note_ = note
  #listName_ = listName
  #listId_ = listId

  debug("Begin Effect")
  debug(Arr:join(["EF List Id: " listId]))
  debug(Arr:join(["EF List Name: " listName]))

  // Check if the user already exists in the list
  #validationPayload = {
    listId: listId_
  }

  #validationResponse = Mk:api("users/lists/show", validationPayload)
  $userFound <- Arr:find(validationResponse.userIds, @(item) { (item = note_.user.id ) } )

  // Another way to do it...
  //~~ (#item, validationResponse.userIds) {
  //  ? (item = note_.user.id) {
  //    userFound <- yes
  //  }
  //}

  ? (userFound != _) {
      Mk:dialog("Not Added" Arr:join(["User: '" note_.user.username "' was already in the list."] "info"))
  } . {
    #payLoad = {
      listId: listId_,
      userId: note_.user.id
    }

    #response = Mk:api("users/lists/push", payLoad)

    debug(Json:stringify(response))

    #message = Arr:join([note_.user.username " was added to the list: " listName_ "."])

    Mk:dialog("Added" message "success")

  }

  debug("End Effect")

  note_
}

@generalHandler(_note, _listName, _listId) {
  // Handler should assure all properties are valid before turning over to the effect
  debug("Begin Handler")

  $note <- _note
  $listName <- _listName
  $listId <- _listId

  debug(Arr:join(["H List Id: " listId]))
  debug(Arr:join(["H List Name: " listName]))

  note <- effect(note, listName, listId)

  debug("End Handler")

  note

}

// Break code into a handler function and an effect function for clarity
Plugin:register_note_action(c.list01Name @(note) {
  generalHandler(note, Plugin:config.list01Name, c.list01Id)
})

? ((c.list02Id != _) & (Str:len(c.list02Id) > 0)) {
  Plugin:register_note_action(c.list02Name @(note) {
    generalHandler(note, Plugin:config.list02Name, c.list02Id)
  })
}

? ((c.list03Id != _) & (Str:len(c.list03Id) > 0)) {
  Plugin:register_note_action(c.list03Name @(note) {
    generalHandler(note, Plugin:config.list03Name, c.list03Id)
  })
}

Developer Deep Dive

These 173 lines of codes were rewritten several times to get to this point. Here are some of the things I learned along the way.

Code in Triplicate

My original plan of action was just to have a single list per plugin. But I soon realized that if a person had one list, they most likely had at least several others. So I decided to allow for up to three lists per plugin. This required a lot of refactoring, and figuring out how to conditionally include Plugin handlers.

If a user only enters a single plugin, and leaves the other two entries blank, there should only be a single entry in the dropdowns. By refactoring some code, I was able to make this happen.

I added conditional statements around each Plugin registration. They only get instantiated and listed in the menu if the list id has been entered.

Note the refactoring. Rather than repeating the same code three times over, I created a “generalHandler” function that takes in the parameters for each list. This function then calls the “effect” function, which does the actual work of adding the user to the list, and is also shared by all three handlers calls. This is a much cleaner approach, and different from previous plugins I’ve written.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
? ((c.list01Id != _) & (Str:len(c.list01Id) > 0)) {
  Plugin:register_note_action(c.list01Name @(note) {
    generalHandler(note, Plugin:config.list01Name, c.list01Id)
  })
}

? ((c.list02Id != _) & (Str:len(c.list02Id) > 0)) {
  Plugin:register_note_action(c.list02Name @(note) {
    generalHandler(note, Plugin:config.list02Name, c.list02Id)
  })
}

? ((c.list03Id != _) & (Str:len(c.list03Id) > 0)) {
  Plugin:register_note_action(c.list03Name @(note) {
    generalHandler(note, Plugin:config.list03Name, c.list03Id)
  })
}

Sequential Code

I am not sure I’ve ever run into this with other languages, but I had to add code in sequential order. If a line of code calls a function that wasn’t defined ABOVE it, it throws an error. So I had to make sure I defined the functions in the order they were called, which means you kind of have to read the code in reverse order for it to make sense (bottom to top).

Api Calls - Fun but Challenging

I started learning how to use Mk:api to make calls and pull back data, like the user’s full set of lists. So, why don’t I just pull back ALL the lists and user has and do all the work of populating the menu actions automatically?

There a few issue here, tho. Some people, like me, have…12 or 15 lists! It’s not going to be performant to add all those lists to a menu. How would I make a decision about which lists to include? I decided to leave that up to the user.

Another issue with this is network traffic. The API call to pull down a user’s lists ALSO includes every single user in each list. This is a lot of data to pull down. When would the data be pulled down? If I cached it, when and how would it get updated? I don’t have answers to those questions, yet. So I left this as is, with users entering in the list Ids manually.

Finally, Mk:api calls don’t seem to have much built in assistance for handling errors. If the Api call fails, there’s not way to trap that error, check the issue, and throw an error message. In fact, if the Api call errors out, nothing is sent back to the front end and the script stops working (crash)! You do see the error in the console, but the Api stops working.

I decided to make an additional Api call to avoid a common trap - when a user tries to add an account that already exists in a list. So, now I check if the user is already in the list, and if so, I throw an error message. This is a much better user experience than just crashing the script.

Api Permissions

In order to add accounts to a list, I have to request both “read:account” and “write:account” permissions. That’s quite an ask! Most users aren’t going to give this a second thought and just accept the permissions. But they really should not. This is different that using Elk or Phanpy and entering your credentials. These scripts can be found in notes on the server, or on a website in japanese, and users have no idea what they do other than allow them to do something fun or useful.

This is a real concern for me. The code I’m providing is harmless, but I could make a mistake or my code could be copied, changed to do something malicous, and re-shared through notes or other websites.

Firefish and Misskey both need to have a catalog of vetted plugins, themes, even css scripts. I plan on recommending this to the developers, and I hope they implement it. I would love to see a catalog of vetted scripts that users can install with a single click, and know that they are safe.

Arrays and Iterations

This was also my first time to use arrays and iterations in AiScript. I needed to iterate over the list to see if the user was already included. I had to learn how to use the Arr:find function, and how to iterate over an array of objects. I also had to learn how to use the Mk:Api function to pull down the user’s lists.

The syntax is unique and very particular, so I hope by adding these notes it can help others avoid a few hours of frustration.

For example, you can test if a value exists in an array, but there’s very little documentation (a single line) and no examples. I had to figure out how to use it by trial and error. Here’s how you can test if a value exists in an array. Here, I call the api to return a user’s lists, then iterate through them to see if the user is already in the list.

Note that the returned Api response is Json, but..I’m parsing through the json like it’s an array.

1
2
3
4
5
6
// Call the api, the iterate through the array of userIds returned, looking for the note user id
// If it's found, the userId value is returned (not true or false)
  #validationResponse = Mk:api("users/lists/show", validationPayload)
  $userFound <- Arr:find(validationResponse.userIds, @(item) { (item = note_.user.id ) } )

Another way to iterate over the same data would just be a normal for each, but of course the syntax is weird.

1
2
3
4
5
6
7
8
// For each userId, pass the current value to the item variable then check if the item is equal to the note user id
~~ (#item, validationResponse.userIds) {
  ? (item = note_.user.id) {
    userFound <- yes
  }
}

I hope these developer notes were helpful. If you have any questions, please feel free to reach out to me on the fediverse @box464@mastodon.social.

This post is licensed under CC BY 4.0 by the author.