Module: post

The post command actually uses your credentials to post a few toots of the stats we gathered. It threads them. The first one goes out with root visibility, and all the others are replies to it with the thread visibility.

The JSON files from analyse need to exist.

Module for posting toots that summarise what's happening.

NOTE: You can have values like api_base_url, username, or cred_file in the [post] section of the config file and they will override the general options. I use that for testing, where I will fetch() from one server, but post() to another so I don't muck-up people's timelines with test toots.

post(config)

Expects the config. Uses that to infer where the analysis file and all the other data is. Builds up the HTML for all the stuff we will post, then calls post_toots() to do the posting.

Config Parameters Used

Option Description
post:galleryurl URL for the gallery to link in the output.
mastoscore:top_n How many top toots we will be reporting
post:tag_users Whether we tag users with an @ or not

Parameters:

Name Type Description Default
config ConfigParser

A ConfigParser object from the config module

required

Returns:

Type Description
int

Number of toots tooted. Also updates the analysis file with the URLs for

int

the posts.

Source code in mastoscore/post.py
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
def post(config:ConfigParser) -> int:
    """
    Expects the config. Uses that to infer where the analysis file and all
    the other data is. Builds up the HTML for all the stuff we will post, then
    calls post_toots() to do the posting.

    ## Config Parameters Used

    | Option | Description |
    | ------- | ------- |
    | `post:galleryurl` | URL for the gallery to link in the output.
    | `mastoscore:top_n` | How many top toots we will be reporting
    | `post:tag_users` | Whether we tag users with an @ or not |

    Args:
      config: A ConfigParser object from the [config](module-config.md) module

    Returns:
      Number of toots tooted. Also updates the analysis file with the URLs for
      the posts.

    """

    debug = get_debug_level(config, "mastoscore")
    top_n = config.getint("mastoscore", "top_n")
    tag_users = config.getboolean("post", "tag_users")
    gallery_url = config.get("post", "galleryurl", fallback=None)

    logger = logging.getLogger(__name__)
    logging.basicConfig(
        format="%(asctime)s %(levelname)-8s %(message)s",
        level=logging.ERROR,
        datefmt="%H:%M:%S",
    )
    logger.setLevel(debug)

    analysis = read_json(config, "analysis")
    if not analysis:
        logger.error("Couldn't open JSON file")
        return 0

    tootlist = []
    text = analysis["preamble"]
    text = (
        text + "<ul>"
        f"<li>{analysis['num_toots']}</li>"
        f"<li>{analysis['most_toots']}</li>"
        "</ul><p>Additional details are replies to this toot."
    )

    # Add gallery link if URL is configured
    if gallery_url:
        text += f' For stats from past watch parties, <a href="{gallery_url}">visit the gallery.</a>'

    text += "</p>"
    tootlist.append(text)

    top = analysis["max_boosts"]
    tag = "@" if tag_users else ""
    text = f"<p>The top {top_n} boosted toots:</p><ol>"
    for post in top:
        text = (
            text + f'<li><a href="{post["url"]}">This toot</a> from '
            f'<a href="{post["account.url"]}">{post["account.display_name"]} '
            f"({tag}{post['userid']})</a>"
            f" had {post['reblogs_count']} boosts.</li>\n"
        )
    text = text + "</ol>"
    tootlist.append(text)

    # Now post about faves
    top = analysis["max_faves"]
    text = f"<p>The top {top_n} favourited toots:</p><ol>"
    for post in top:
        text = (
            text + f'<li><a href="{post["url"]}">This toot</a> from '
            f'<a href="{post["account.url"]}">{post["account.display_name"]} '
            f"({tag}{post['userid']})</a>"
            f" had {post['favourites_count']} favourites.</li>\n"
        )
    text = text + "</ol>"
    tootlist.append(text)

    # Now post about replies
    top = analysis["max_replies"]
    text = f"<p>The top {top_n} most-replied-to toots:</p><ol>"
    for post in top:
        text = (
            text + f'<li><a href="{post["url"]}">This toot</a> from '
            f'<a href="{post["account.url"]}">{post["account.display_name"]} '
            f"({tag}{post['userid']})</a>"
            f" had {post['replies_count']} replies.</li>\n"
        )
    text = text + "</ol>"
    tootlist.append(text)

    tootids = {}
    tootids["tootlist"], post_urls = post_toots(config, tootlist)

    # Save post URLs to data-{hashtag}-posts.json
    # Use write_json to create new file (overwrites if exists)
    write_json(config, "posts", {"text_posts": post_urls})
    # This will open up the hashtag-results.json file and insert all the IDs for all
    # the toots into it. That will be used in the blog and in the graph posting.
    update_json(config, "analysis", tootids)
    return len(tootlist)

post_all(config)

Post analysis results and graphs to Mastodon. Combines post() and post_graphs() functionality.

Parameters:

Name Type Description Default
config ConfigParser

ConfigParser object with all configuration

required

Returns: Number of toots posted (text + graphs)

Source code in mastoscore/post.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
def post_all(config:ConfigParser) -> int:
    """
    Post analysis results and graphs to Mastodon.
    Combines post() and post_graphs() functionality.

    Args:
      config: ConfigParser object with all configuration

    Returns:
    Number of toots posted (text + graphs)
    """
    logger = logging.getLogger(__name__)

    # First post the analysis text
    try:
        text_count = post(config)
        logger.info(f"Posted {text_count} text posts")
    except Exception as e:
        logger.error(f"Failed to post text: {e}")
        raise

    # Then post the graphs
    try:
        graph_count = post_graphs(config)
        logger.info(f"Posted {graph_count} graph posts")
    except Exception as e:
        logger.error(f"Failed to post graphs: {e}")
        # Don't raise - text posts already succeeded
        graph_count = 0

    return text_count + graph_count

post_toots(config, tootlist)

Given the config and list of toots, create a Tooter and toot them.

Config Parameters Used

Option Description
post:root_visibility The very first post in the thread will be set to this visibility. Usually 'public'. Must be a valid string for mastodon statuses which currently are: public, unlisted, private, and direct.
post:thread_visibility All toots are tooted as replies to each other. root → first → second, etc. All posts other than the root will have thread_visibility visibility.
post:hashtag We tag all the toots with the hashtag.
post:api_base_url Implicitly used when we create our Tooter
post:cred_file Implicitly used when we create our Tooter

Parameters:

Name Type Description Default
config ConfigParser

A ConfigParser object from the config module

required
tootlist List[str]

A list of strings which are the toots to be tooted. This module doesn't change them.

required

Returns:

Type Description
List[str]

Number of toots tooted.

TODO

Take the maximum post size into account and break into multiple toots.

Source code in mastoscore/post.py
 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
def post_toots(config:ConfigParser, tootlist:List[str]) -> List[str]:
    """
    Given the config and list of toots, create a [Tooter](module-tooter.md) and toot them.

    ## Config Parameters Used
    | Option | Description |
    | ------- | ------- |
    | `post:root_visibility` | The very first post in the thread will be set to this visibility. Usually 'public'. Must be a valid string for [mastodon statuses](https://docs.joinmastodon.org/methods/statuses/#form-data-parameters) which currently are: `public`, `unlisted`, `private`, and `direct`. |
    | `post:thread_visibility` | All toots are tooted as replies to each other. root &rarr; first &rarr; second, etc. All posts other than the root will have `thread_visibility` visibility. |
    | `post:hashtag` | We tag all the toots with the hashtag. |
    | `post:api_base_url` | Implicitly used when we create our [Tooter](module-tooter.md) |
    | `post:cred_file` | Implicitly used when we create our [Tooter](module-tooter.md) |

    Args:
      config: A ConfigParser object from the [config](module-config.md) module
      tootlist: A list of strings which are the toots to be tooted. This module doesn't change them.

    Returns:
      Number of toots tooted.

    ## TODO

    Take the maximum post size into account and break into multiple toots.

    """
    debug = get_debug_level(config, "mastoscore")
    root_visibility = config.get("post", "root_visibility")
    thread_visibility = config.get("post", "thread_visibility")
    hashtag = config.get("mastoscore", "hashtag")

    logger = logging.getLogger(__name__)
    logging.basicConfig(
        format="%(asctime)s %(levelname)-8s %(message)s",
        level=logging.ERROR,
        datefmt="%H:%M:%S",
    )
    logger.setLevel(debug)

    try:
        t = Tooter(config, "post")
    except Exception as e:
        logger.critical("Failed to create 'post' Tooter")
        logger.critical(e)
        return []

    reply = None
    next_status = {}
    n = 1

    # First an anchor post with some details
    uidlist = []
    post_urls = []
    for toot in tootlist:
        visibility = root_visibility if n == 1 else thread_visibility
        text = toot + f"<p>#{hashtag} {n}/{len(tootlist) + 1}</p>"
        post_name = "root post" if n == 1 else f"text post {n}"

        if t is None:
            print(f"{text}\nVisibility: {visibility}")
            next_status["id"] = n
            next_status["url"] = f"http://example.com/status/{n}"
            uidlist.append(str(n))
            post_urls.append({"name": post_name, "url": next_status["url"]})
        else:
            try:
                next_status = t.status_post(
                    text,
                    visibility=visibility,
                    language="en",
                    in_reply_to_id=reply,
                    content_type="text/html",
                )
                uidlist.append(str(next_status["id"]))
                post_urls.append({"name": post_name, "url": next_status["url"]})
                logger.info(f"anchor: {n}")
            except Exception as e:
                logger.error("anchor post")
                logger.error(e)
                return uidlist, post_urls
        n = n + 1
        reply = next_status["id"]
    logger.info(f"Posted {n} toots")

    return uidlist, post_urls