commit 7e743e8918e0a4d058217ec33ab745b1f1a97335
parent cbd89bdf16674b577cee7d7533ea69014cb307e7
Author: shtrophic <christoph@liebender.dev>
Date: Thu, 5 Dec 2024 17:16:57 +0100
Merge remote-tracking branch 'upstream/master'
Diffstat:
M | RELEASE_NOTES.md | | | 16 | ++++++++++++++++ |
M | activitypub.c | | | 101 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------- |
M | data.c | | | 119 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | doc/snac.1 | | | 36 | +++++++++++++++++++++++++++++++++++- |
M | html.c | | | 230 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------- |
M | main.c | | | 16 | +++++++++++++++- |
M | mastoapi.c | | | 25 | +++++++++++++++++++++++++ |
M | snac.h | | | 13 | ++++++++++++- |
M | utils.c | | | 53 | ++++++++++++++++++++++++++++++++++++++++++++++------- |
M | xs_url.h | | | 8 | ++++---- |
M | xs_version.h | | | 2 | +- |
11 files changed, 543 insertions(+), 76 deletions(-)
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
@@ -1,5 +1,21 @@
# Release Notes
+## UNRELEASED
+
+As many users have asked for it, there is now an option to make the number of followed and following accounts public (still disabled by default). These are only the numbers; the lists themselves are never published.
+
+Some fixes to blocked instances code (posts from them were sometimes shown).
+
+## 2.65
+
+Added a new user option to disable automatic follow confirmations (follow requests must be manually approved from the people page).
+
+The search box also searches for accounts (via webfinger).
+
+New command-line action `import_list`, to import a Mastodon list in CSV format (so that [Mastodon Follow Packs](https://mastodonmigration.wordpress.com/?p=995) can be directly used).
+
+New command-line action `import_block_list`, to import a Mastodon list of accounts to be blocked in CSV format.
+
## 2.64
Some tweaks for better integration with https://bsky.brid.gy (the BlueSky bridge by brid.gy).
diff --git a/activitypub.c b/activitypub.c
@@ -1038,15 +1038,14 @@ xs_dict *msg_base(snac *snac, const char *type, const char *id,
}
-xs_dict *msg_collection(snac *snac, const char *id)
+xs_dict *msg_collection(snac *snac, const char *id, int items)
/* creates an empty OrderedCollection message */
{
xs_dict *msg = msg_base(snac, "OrderedCollection", id, NULL, NULL, NULL);
- xs *ol = xs_list_new();
+ xs *n = xs_number_new(items);
msg = xs_dict_append(msg, "attributedTo", snac->actor);
- msg = xs_dict_append(msg, "orderedItems", ol);
- msg = xs_dict_append(msg, "totalItems", xs_stock(0));
+ msg = xs_dict_append(msg, "totalItems", n);
return msg;
}
@@ -1218,7 +1217,30 @@ xs_dict *msg_actor(snac *snac)
}
/* add the metadata as attachments of PropertyValue */
- const xs_dict *metadata = xs_dict_get(snac->config, "metadata");
+ xs *metadata = NULL;
+ const xs_dict *md = xs_dict_get(snac->config, "metadata");
+
+ if (xs_type(md) == XSTYPE_DICT)
+ metadata = xs_dup(md);
+ else
+ if (xs_type(md) == XSTYPE_STRING) {
+ metadata = xs_dict_new();
+ xs *l = xs_split(md, "\n");
+ const char *ll;
+
+ xs_list_foreach(l, ll) {
+ xs *kv = xs_split_n(ll, "=", 1);
+ const char *k = xs_list_get(kv, 0);
+ const char *v = xs_list_get(kv, 1);
+
+ if (k && v) {
+ xs *kk = xs_strip_i(xs_dup(k));
+ xs *vv = xs_strip_i(xs_dup(v));
+ metadata = xs_dict_set(metadata, kk, vv);
+ }
+ }
+ }
+
if (xs_type(metadata) == XSTYPE_DICT) {
xs *attach = xs_list_new();
const xs_str *k;
@@ -1264,6 +1286,10 @@ xs_dict *msg_actor(snac *snac)
msg = xs_dict_set(msg, "alsoKnownAs", loaka);
}
+ const xs_val *manually = xs_dict_get(snac->config, "approve_followers");
+ msg = xs_dict_set(msg, "manuallyApprovesFollowers",
+ xs_stock(xs_is_true(manually) ? XSTYPE_TRUE : XSTYPE_FALSE));
+
return msg;
}
@@ -1900,22 +1926,31 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
object_add(actor, actor_obj);
}
- xs *f_msg = xs_dup(msg);
- xs *reply = msg_accept(snac, f_msg, actor);
+ if (xs_is_true(xs_dict_get(snac->config, "approve_followers"))) {
+ pending_add(snac, actor, msg);
- post_message(snac, actor, reply);
-
- if (xs_is_null(xs_dict_get(f_msg, "published"))) {
- /* add a date if it doesn't include one (Mastodon) */
- xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
- f_msg = xs_dict_set(f_msg, "published", date);
+ snac_log(snac, xs_fmt("new pending follower approval %s", actor));
}
+ else {
+ /* automatic following */
+ xs *f_msg = xs_dup(msg);
+ xs *reply = msg_accept(snac, f_msg, actor);
+
+ post_message(snac, actor, reply);
+
+ if (xs_is_null(xs_dict_get(f_msg, "published"))) {
+ /* add a date if it doesn't include one (Mastodon) */
+ xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
+ f_msg = xs_dict_set(f_msg, "published", date);
+ }
+
+ timeline_add(snac, id, f_msg);
- timeline_add(snac, id, f_msg);
+ follower_add(snac, actor);
- follower_add(snac, actor);
+ snac_log(snac, xs_fmt("new follower %s", actor));
+ }
- snac_log(snac, xs_fmt("new follower %s", actor));
do_notify = 1;
}
else
@@ -1937,6 +1972,11 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
do_notify = 1;
}
else
+ if (pending_check(snac, actor)) {
+ pending_del(snac, actor);
+ snac_log(snac, xs_fmt("cancelled pending follow from %s", actor));
+ }
+ else
snac_log(snac, xs_fmt("error deleting follower %s", actor));
}
}
@@ -2796,6 +2836,8 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
*ctype = "application/activity+json";
+ int show_contact_metrics = xs_is_true(xs_dict_get(snac.config, "show_contact_metrics"));
+
if (p_path == NULL) {
/* if there was no component after the user, it's an actor request */
msg = msg_actor(&snac);
@@ -2809,7 +2851,6 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
if (strcmp(p_path, "outbox") == 0 || strcmp(p_path, "featured") == 0) {
xs *id = xs_fmt("%s/%s", snac.actor, p_path);
xs *list = xs_list_new();
- msg = msg_collection(&snac, id);
const char *v;
int tc = 0;
@@ -2831,14 +2872,32 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
}
/* replace the 'orderedItems' with the latest posts */
- xs *items = xs_number_new(xs_list_len(list));
+ msg = msg_collection(&snac, id, xs_list_len(list));
msg = xs_dict_set(msg, "orderedItems", list);
- msg = xs_dict_set(msg, "totalItems", items);
}
else
- if (strcmp(p_path, "followers") == 0 || strcmp(p_path, "following") == 0) {
+ if (strcmp(p_path, "followers") == 0) {
+ int total = 0;
+
+ if (show_contact_metrics) {
+ xs *l = follower_list(&snac);
+ total = xs_list_len(l);
+ }
+
+ xs *id = xs_fmt("%s/%s", snac.actor, p_path);
+ msg = msg_collection(&snac, id, total);
+ }
+ else
+ if (strcmp(p_path, "following") == 0) {
+ int total = 0;
+
+ if (show_contact_metrics) {
+ xs *l = following_list(&snac);
+ total = xs_list_len(l);
+ }
+
xs *id = xs_fmt("%s/%s", snac.actor, p_path);
- msg = msg_collection(&snac, id);
+ msg = msg_collection(&snac, id, total);
}
else
if (xs_startswith(p_path, "p/")) {
diff --git a/data.c b/data.c
@@ -299,6 +299,35 @@ int user_persist(snac *snac, int publish)
xs *bfn = xs_fmt("%s.bak", fn);
FILE *f;
+ if (publish) {
+ /* check if any of the relevant fields have really changed */
+ if ((f = fopen(fn, "r")) != NULL) {
+ xs *old = xs_json_load(f);
+ fclose(f);
+
+ if (old != NULL) {
+ int nw = 0;
+ const char *fields[] = { "header", "avatar", "name", "bio", "metadata", NULL };
+
+ for (int n = 0; fields[n]; n++) {
+ const char *of = xs_dict_get(old, fields[n]);
+ const char *nf = xs_dict_get(snac->config, fields[n]);
+
+ if (of == NULL && nf == NULL)
+ continue;
+
+ if (xs_type(of) != XSTYPE_STRING || xs_type(nf) != XSTYPE_STRING || strcmp(of, nf)) {
+ nw = 1;
+ break;
+ }
+ }
+
+ if (!nw)
+ publish = 0;
+ }
+ }
+ }
+
rename(fn, bfn);
if ((f = fopen(fn, "w")) != NULL) {
@@ -1139,6 +1168,96 @@ xs_list *follower_list(snac *snac)
}
+/** pending followers **/
+
+int pending_add(snac *user, const char *actor, const xs_dict *msg)
+/* stores the follow message for later confirmation */
+{
+ xs *dir = xs_fmt("%s/pending", user->basedir);
+ xs *md5 = xs_md5_hex(actor, strlen(actor));
+ xs *fn = xs_fmt("%s/%s.json", dir, md5);
+ FILE *f;
+
+ mkdirx(dir);
+
+ if ((f = fopen(fn, "w")) == NULL)
+ return -1;
+
+ xs_json_dump(msg, 4, f);
+ fclose(f);
+
+ return 0;
+}
+
+
+int pending_check(snac *user, const char *actor)
+/* checks if there is a pending follow confirmation for the actor */
+{
+ xs *md5 = xs_md5_hex(actor, strlen(actor));
+ xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
+
+ return mtime(fn) != 0;
+}
+
+
+xs_dict *pending_get(snac *user, const char *actor)
+/* returns the pending follow confirmation for the actor */
+{
+ xs *md5 = xs_md5_hex(actor, strlen(actor));
+ xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
+ xs_dict *msg = NULL;
+ FILE *f;
+
+ if ((f = fopen(fn, "r")) != NULL) {
+ msg = xs_json_load(f);
+ fclose(f);
+ }
+
+ return msg;
+}
+
+
+void pending_del(snac *user, const char *actor)
+/* deletes a pending follow confirmation for the actor */
+{
+ xs *md5 = xs_md5_hex(actor, strlen(actor));
+ xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
+
+ unlink(fn);
+}
+
+
+xs_list *pending_list(snac *user)
+/* returns a list of pending follow confirmations */
+{
+ xs *spec = xs_fmt("%s/pending/""*.json", user->basedir);
+ xs *l = xs_glob(spec, 0, 0);
+ xs_list *r = xs_list_new();
+ const char *v;
+
+ xs_list_foreach(l, v) {
+ FILE *f;
+ xs *msg = NULL;
+
+ if ((f = fopen(v, "r")) == NULL)
+ continue;
+
+ msg = xs_json_load(f);
+ fclose(f);
+
+ if (msg == NULL)
+ continue;
+
+ const char *actor = xs_dict_get(msg, "actor");
+
+ if (xs_type(actor) == XSTYPE_STRING)
+ r = xs_list_append(r, actor);
+ }
+
+ return r;
+}
+
+
/** timeline **/
double timeline_mtime(snac *snac)
diff --git a/doc/snac.1 b/doc/snac.1
@@ -129,6 +129,28 @@ Just what it says in the tin. This is to mitigate spammers
coming from Fediverse instances with lax / open registration
processes. Please take note that this also avoids possibly
legitimate people trying to contact you.
+.It This account is a bot
+Set this checkbox if this account behaves like a bot (i.e.
+posts are automatically generated).
+.It Auto-boost all mentions to this account
+If this toggle is set, all mentions to this account are boosted
+to all followers. This can be used to create groups.
+.It This account is private
+If this toggle is set, posts are not published via the public
+web interface, only via the ActivityPub protocol.
+.It Collapse top threads by default
+If this toggle is set, the private timeline will always show
+conversations collapsed by default. This allows easier navigation
+through long threads.
+.It Follow requests must be approved
+If this toggle is set, follow requests are not automatically
+accepted, but notified and stored for later review. Pending
+follow requests will be shown in the people page to be
+approved or discarded.
+.It Publish follower and following metrics
+If this toggle is set, the number of followers and following
+accounts are made public (this is only the number; the specific
+lists of accounts are never published).
.It Password
Write the same string in these two fields to change your
password. Don't write anything if you don't want to do this.
@@ -262,6 +284,13 @@ section 'Migrating from snac to Mastodon').
Starts a migration from this account to the one set as an alias (see
.Xr snac 8 ,
section 'Migrating from snac to Mastodon').
+.It Cm import_csv Ar basedir Ar uid
+Imports CSV data files from a Mastodon export. This command expects the
+following files to be in the current directory:
+.Pa bookmarks.csv ,
+.Pa blocked_accounts.csv ,
+.Pa lists.csv , and
+.Pa following_accounts.csv .
.It Cm state Ar basedir
Dumps the current state of the server and its threads. For example:
.Bd -literal -offset indent
@@ -284,6 +313,11 @@ in-memory job queue. The thread state can be: waiting (idle waiting
for a job to be assigned), input or output (processing I/O packets)
or stopped (not running, only to be seen while starting or stopping
the server).
+.It Cm import_list Ar basedir Ar uid Ar file
+Imports a Mastodon list in CSV format. This option can be used to
+import "Mastodon Follow Packs".
+.It Cm import_block_list Ar basedir Ar uid Ar file
+Imports a Mastodon list of accounts to be blocked in CSV format.
.El
.Ss Migrating an account to/from Mastodon
See
@@ -349,4 +383,4 @@ See the LICENSE file for details.
.Sh CAVEATS
Use the Fediverse sparingly. Don't fear the MUTE button.
.Sh BUGS
-Probably plenty. Some issues may be even documented in the TODO.md file.
+Probably many. Some issues may be even documented in the TODO.md file.
diff --git a/html.c b/html.c
@@ -770,7 +770,7 @@ static xs_html *html_user_body(snac *user, int read_only)
xs_html_sctag("input",
xs_html_attr("type", "text"),
xs_html_attr("name", "q"),
- xs_html_attr("title", L("Search posts by content (regular expression) or #tag")),
+ xs_html_attr("title", L("Search posts by content (regular expression), @user@host accounts, or #tag")),
xs_html_attr("placeholder", L("Content search")))));
}
@@ -829,21 +829,45 @@ static xs_html *html_user_body(snac *user, int read_only)
}
if (read_only) {
- xs *es1 = encode_html(xs_dict_get(user->config, "bio"));
xs *tags = xs_list_new();
- xs *bio1 = not_really_markdown(es1, NULL, &tags);
+ xs *bio1 = not_really_markdown(xs_dict_get(user->config, "bio"), NULL, &tags);
xs *bio2 = process_tags(user, bio1, &tags);
+ xs *bio3 = sanitize(bio2);
- bio2 = replace_shortnames(bio2, tags, 2, proxy);
+ bio3 = replace_shortnames(bio3, tags, 2, proxy);
xs_html *top_user_bio = xs_html_tag("div",
xs_html_attr("class", "p-note snac-top-user-bio"),
- xs_html_raw(bio2)); /* already sanitized */
+ xs_html_raw(bio3)); /* already sanitized */
xs_html_add(top_user,
top_user_bio);
- const xs_dict *metadata = xs_dict_get(user->config, "metadata");
+ xs *metadata = NULL;
+ const xs_dict *md = xs_dict_get(user->config, "metadata");
+
+ if (xs_type(md) == XSTYPE_DICT)
+ metadata = xs_dup(md);
+ else
+ if (xs_type(md) == XSTYPE_STRING) {
+ /* convert to dict for easier iteration */
+ metadata = xs_dict_new();
+ xs *l = xs_split(md, "\n");
+ const char *ll;
+
+ xs_list_foreach(l, ll) {
+ xs *kv = xs_split_n(ll, "=", 1);
+ const char *k = xs_list_get(kv, 0);
+ const char *v = xs_list_get(kv, 1);
+
+ if (k && v) {
+ xs *kk = xs_strip_i(xs_dup(k));
+ xs *vv = xs_strip_i(xs_dup(v));
+ metadata = xs_dict_set(metadata, kk, vv);
+ }
+ }
+ }
+
if (xs_type(metadata) == XSTYPE_DICT) {
const xs_str *k;
const xs_str *v;
@@ -914,6 +938,18 @@ static xs_html *html_user_body(snac *user, int read_only)
xs_html_add(top_user,
snac_metadata);
}
+
+ if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
+ xs *fwers = follower_list(user);
+ xs *fwing = following_list(user);
+
+ xs *s1 = xs_fmt(L("%d following %d followers"),
+ xs_list_len(fwing), xs_list_len(fwers));
+
+ xs_html_add(top_user,
+ xs_html_tag("p",
+ xs_html_text(s1)));
+ }
}
xs_html_add(body,
@@ -1025,20 +1061,31 @@ xs_html *html_top_controls(snac *snac)
const xs_val *a_private = xs_dict_get(snac->config, "private");
const xs_val *auto_boost = xs_dict_get(snac->config, "auto_boost");
const xs_val *coll_thrds = xs_dict_get(snac->config, "collapse_threads");
+ const xs_val *pending = xs_dict_get(snac->config, "approve_followers");
+ const xs_val *show_foll = xs_dict_get(snac->config, "show_contact_metrics");
- xs *metadata = xs_str_new(NULL);
+ xs *metadata = NULL;
const xs_dict *md = xs_dict_get(snac->config, "metadata");
- const xs_str *k;
- const xs_str *v;
- int c = 0;
- while (xs_dict_next(md, &k, &v, &c)) {
- xs *kp = xs_fmt("%s=%s", k, v);
+ if (xs_type(md) == XSTYPE_DICT) {
+ const xs_str *k;
+ const xs_str *v;
- if (*metadata)
- metadata = xs_str_cat(metadata, "\n");
- metadata = xs_str_cat(metadata, kp);
+ metadata = xs_str_new(NULL);
+
+ xs_dict_foreach(md, k, v) {
+ xs *kp = xs_fmt("%s=%s", k, v);
+
+ if (*metadata)
+ metadata = xs_str_cat(metadata, "\n");
+ metadata = xs_str_cat(metadata, kp);
+ }
}
+ else
+ if (xs_type(md) == XSTYPE_STRING)
+ metadata = xs_dup(md);
+ else
+ metadata = xs_str_new(NULL);
xs *user_setup_action = xs_fmt("%s/admin/user-setup", snac->actor);
@@ -1188,6 +1235,24 @@ xs_html *html_top_controls(snac *snac)
xs_html_attr("for", "collapse_threads"),
xs_html_text(L("Collapse top threads by default")))),
xs_html_tag("p",
+ xs_html_sctag("input",
+ xs_html_attr("type", "checkbox"),
+ xs_html_attr("name", "approve_followers"),
+ xs_html_attr("id", "approve_followers"),
+ xs_html_attr(xs_is_true(pending) ? "checked" : "", NULL)),
+ xs_html_tag("label",
+ xs_html_attr("for", "approve_followers"),
+ xs_html_text(L("Follow requests must be approved")))),
+ xs_html_tag("p",
+ xs_html_sctag("input",
+ xs_html_attr("type", "checkbox"),
+ xs_html_attr("name", "show_contact_metrics"),
+ xs_html_attr("id", "show_contact_metrics"),
+ xs_html_attr(xs_is_true(show_foll) ? "checked" : "", NULL)),
+ xs_html_tag("label",
+ xs_html_attr("for", "show_contact_metrics"),
+ xs_html_text(L("Publish follower and following metrics")))),
+ xs_html_tag("p",
xs_html_text(L("Profile metadata (key=value pairs in each line):")),
xs_html_sctag("br", NULL),
xs_html_tag("textarea",
@@ -1481,6 +1546,9 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
if ((read_only || !user) && !is_msg_public(msg))
return NULL;
+ if (id && is_instance_blocked(id))
+ return NULL;
+
if (user && level == 0 && xs_is_true(xs_dict_get(user->config, "collapse_threads")))
collapse_threads = 1;
@@ -2437,10 +2505,9 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
xs_html_tag("summary",
xs_html_text("..."))));
- xs_list *p = list;
const char *actor_id;
- while (xs_list_iter(&p, &actor_id)) {
+ xs_list_foreach(list, actor_id) {
xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
xs *actor = NULL;
@@ -2509,6 +2576,15 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
html_button("limit", L("Limit"),
L("Block announces (boosts) from this user")));
}
+ else
+ if (pending_check(snac, actor_id)) {
+ xs_html_add(form,
+ html_button("approve", L("Approve"),
+ L("Approve this follow request")));
+
+ xs_html_add(form,
+ html_button("discard", L("Discard"), L("Discard this follow request")));
+ }
else {
xs_html_add(form,
html_button("follow", L("Follow"),
@@ -2563,13 +2639,23 @@ xs_str *html_people(snac *user)
xs *wing = following_list(user);
xs *wers = follower_list(user);
+ xs_html *lists = xs_html_tag("div",
+ xs_html_attr("class", "snac-posts"));
+
+ if (xs_is_true(xs_dict_get(user->config, "approve_followers"))) {
+ xs *pending = pending_list(user);
+ xs_html_add(lists,
+ html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy));
+ }
+
+ xs_html_add(lists,
+ html_people_list(user, wing, L("People you follow"), "i", proxy),
+ html_people_list(user, wers, L("People that follow you"), "e", proxy));
+
xs_html *html = xs_html_tag("html",
html_user_head(user, NULL, NULL),
xs_html_add(html_user_body(user, 0),
- xs_html_tag("div",
- xs_html_attr("class", "snac-posts"),
- html_people_list(user, wing, L("People you follow"), "i", proxy),
- html_people_list(user, wers, L("People that follow you"), "e", proxy)),
+ lists,
html_footer()));
return xs_html_render_s(html, "<!DOCTYPE html>\n");
@@ -2661,6 +2747,9 @@ xs_str *html_notifications(snac *user, int skip, int show)
label = wrk;
}
}
+ else
+ if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id))
+ label = L("Follow Request");
xs *s_date = xs_crop_i(xs_dup(date), 0, 10);
@@ -2909,6 +2998,48 @@ int html_get_handler(const xs_dict *req, const char *q_path,
const char *q = xs_dict_get(q_vars, "q");
if (q && *q) {
+ if (xs_regex_match(q, "^@?[a-zA-Z0-9_]+@[a-zA-Z0-9-]+\\.")) {
+ /** search account **/
+ xs *actor = NULL;
+ xs *acct = NULL;
+ xs *l = xs_list_new();
+ xs_html *page = NULL;
+
+ if (valid_status(webfinger_request(q, &actor, &acct))) {
+ xs *actor_obj = NULL;
+
+ if (valid_status(actor_request(&snac, actor, &actor_obj))) {
+ actor_add(actor, actor_obj);
+
+ /* create a people list with only one element */
+ l = xs_list_append(xs_list_new(), actor);
+
+ xs *title = xs_fmt(L("Search results for account %s"), q);
+
+ page = html_people_list(&snac, l, title, "wf", NULL);
+ }
+ }
+
+ if (page == NULL) {
+ xs *title = xs_fmt(L("Account %s not found"), q);
+
+ page = xs_html_tag("div",
+ xs_html_tag("h2",
+ xs_html_attr("class", "snac-header"),
+ xs_html_text(title)));
+ }
+
+ xs_html *html = xs_html_tag("html",
+ html_user_head(&snac, NULL, NULL),
+ xs_html_add(html_user_body(&snac, 0),
+ page,
+ html_footer()));
+
+ *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
+ *b_size = strlen(*body);
+ status = HTTP_STATUS_OK;
+ }
+ else
if (*q == '#') {
/** search by tag **/
xs *tl = tag_search(q, skip, show + 1);
@@ -3647,6 +3778,34 @@ int html_post_handler(const xs_dict *req, const char *q_path,
timeline_touch(&snac);
}
else
+ if (strcmp(action, L("Approve")) == 0) { /** **/
+ xs *fwreq = pending_get(&snac, actor);
+
+ if (fwreq != NULL) {
+ xs *reply = msg_accept(&snac, fwreq, actor);
+
+ enqueue_message(&snac, reply);
+
+ if (xs_is_null(xs_dict_get(fwreq, "published"))) {
+ /* add a date if it doesn't include one (Mastodon) */
+ xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
+ fwreq = xs_dict_set(fwreq, "published", date);
+ }
+
+ timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
+
+ follower_add(&snac, actor);
+
+ pending_del(&snac, actor);
+
+ snac_log(&snac, xs_fmt("new follower %s", actor));
+ }
+ }
+ else
+ if (strcmp(action, L("Discard")) == 0) { /** **/
+ pending_del(&snac, actor);
+ }
+ else
status = HTTP_STATUS_NOT_FOUND;
/* delete the cached timeline */
@@ -3705,26 +3864,17 @@ int html_post_handler(const xs_dict *req, const char *q_path,
snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_TRUE));
else
snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_FALSE));
+ if ((v = xs_dict_get(p_vars, "approve_followers")) != NULL && strcmp(v, "on") == 0)
+ snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_TRUE));
+ else
+ snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_FALSE));
+ if ((v = xs_dict_get(p_vars, "show_contact_metrics")) != NULL && strcmp(v, "on") == 0)
+ snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE));
+ else
+ snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
- if ((v = xs_dict_get(p_vars, "metadata")) != NULL) {
- /* split the metadata and store it as a dict */
- xs_dict *md = xs_dict_new();
- xs *l = xs_split(v, "\n");
- xs_list *p = l;
- const xs_str *kp;
-
- while (xs_list_iter(&p, &kp)) {
- xs *kpl = xs_split_n(kp, "=", 1);
- if (xs_list_len(kpl) == 2) {
- xs *k2 = xs_strip_i(xs_dup(xs_list_get(kpl, 0)));
- xs *v2 = xs_strip_i(xs_dup(xs_list_get(kpl, 1)));
-
- md = xs_dict_set(md, k2, v2);
- }
- }
-
- snac.config = xs_dict_set(snac.config, "metadata", md);
- }
+ if ((v = xs_dict_get(p_vars, "metadata")) != NULL)
+ snac.config = xs_dict_set(snac.config, "metadata", v);
/* uploads */
const char *uploads[] = { "avatar", "header", NULL };
diff --git a/main.c b/main.c
@@ -51,7 +51,9 @@ int usage(void)
printf("export_csv {basedir} {uid} Exports data as CSV files into current directory\n");
printf("alias {basedir} {uid} {account} Sets account (@user@host or actor url) as an alias\n");
printf("migrate {basedir} {uid} Migrates to the account defined as the alias\n");
- printf("import_csv {basedir} {uid} Imports data from CSV files into current directory\n");
+ printf("import_csv {basedir} {uid} Imports data from CSV files in the current directory\n");
+ printf("import_list {basedir} {uid} {file} Imports a Mastodon CSV list file\n");
+ printf("import_block_list {basedir} {uid} {file} Imports a Mastodon CSV block list file\n");
return 1;
}
@@ -589,6 +591,18 @@ int main(int argc, char *argv[])
return 0;
}
+ if (strcmp(cmd, "import_list") == 0) { /** **/
+ import_list_csv(&snac, url);
+
+ return 0;
+ }
+
+ if (strcmp(cmd, "import_block_list") == 0) { /** **/
+ import_blocked_accounts_csv(&snac, url);
+
+ return 0;
+ }
+
if (strcmp(cmd, "note") == 0) { /** **/
xs *content = NULL;
xs *msg = NULL;
diff --git a/mastoapi.c b/mastoapi.c
@@ -663,6 +663,17 @@ xs_dict *mastoapi_account(snac *logged, const xs_dict *actor)
if (user_open(&user, prefu)) {
val_links = user.links;
metadata = xs_dict_get_def(user.config, "metadata", xs_stock(XSTYPE_DICT));
+
+ /* does this user want to publish their contact metrics? */
+ if (xs_is_true(xs_dict_get(user.config, "show_contact_metrics"))) {
+ xs *fwing = following_list(&user);
+ xs *fwers = follower_list(&user);
+ xs *ni = xs_number_new(xs_list_len(fwing));
+ xs *ne = xs_number_new(xs_list_len(fwers));
+
+ acct = xs_dict_append(acct, "followers_count", ne);
+ acct = xs_dict_append(acct, "following_count", ni);
+ }
}
}
@@ -1275,6 +1286,17 @@ void credentials_get(char **body, char **ctype, int *status, snac snac)
acct = xs_dict_append(acct, "following_count", xs_stock(0));
acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
+ /* does this user want to publish their contact metrics? */
+ if (xs_is_true(xs_dict_get(snac.config, "show_contact_metrics"))) {
+ xs *fwing = following_list(&snac);
+ xs *fwers = follower_list(&snac);
+ xs *ni = xs_number_new(xs_list_len(fwing));
+ xs *ne = xs_number_new(xs_list_len(fwers));
+
+ acct = xs_dict_append(acct, "followers_count", ne);
+ acct = xs_dict_append(acct, "following_count", ni);
+ }
+
*body = xs_json_dumps(acct, 4);
*ctype = "application/json";
*status = HTTP_STATUS_OK;
@@ -1349,6 +1371,9 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
continue;
+ if (id && is_instance_blocked(id))
+ continue;
+
const char *from = NULL;
if (strcmp(type, "Page") == 0)
from = xs_dict_get(msg, "audience");
diff --git a/snac.h b/snac.h
@@ -1,7 +1,7 @@
/* snac - A simple, minimalistic ActivityPub instance */
/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
-#define VERSION "2.64"
+#define VERSION "2.66-dev"
#define USER_AGENT "snac/" VERSION
@@ -143,6 +143,12 @@ int follower_del(snac *snac, const char *actor);
int follower_check(snac *snac, const char *actor);
xs_list *follower_list(snac *snac);
+int pending_add(snac *user, const char *actor, const xs_dict *msg);
+int pending_check(snac *user, const char *actor);
+xs_dict *pending_get(snac *user, const char *actor);
+void pending_del(snac *user, const char *actor);
+xs_list *pending_list(snac *user);
+
double timeline_mtime(snac *snac);
int timeline_touch(snac *snac);
int timeline_here(snac *snac, const char *md5);
@@ -316,6 +322,7 @@ xs_dict *msg_update(snac *snac, const xs_dict *object);
xs_dict *msg_ping(snac *user, const char *rcpt);
xs_dict *msg_pong(snac *user, const char *rcpt, const char *object);
xs_dict *msg_move(snac *user, const char *new_account);
+xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to);
xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
const xs_list *opts, int multiple, int end_secs);
@@ -399,6 +406,10 @@ void verify_links(snac *user);
void export_csv(snac *user);
int migrate_account(snac *user);
+
+void import_blocked_accounts_csv(snac *user, const char *fn);
+void import_following_accounts_csv(snac *user, const char *fn);
+void import_list_csv(snac *user, const char *fn);
void import_csv(snac *user);
typedef enum {
diff --git a/utils.c b/utils.c
@@ -670,20 +670,18 @@ void export_csv(snac *user)
}
-void import_csv(snac *user)
-/* import CSV files from Mastodon */
+void import_blocked_accounts_csv(snac *user, const char *fn)
+/* imports a Mastodon CSV file of blocked accounts */
{
FILE *f;
- const char *fn;
- fn = "blocked_accounts.csv";
if ((f = fopen(fn, "r")) != NULL) {
snac_log(user, xs_fmt("Importing from %s...", fn));
while (!feof(f)) {
xs *l = xs_strip_i(xs_readline(f));
- if (*l) {
+ if (*l && strchr(l, '@') != NULL) {
xs *url = NULL;
xs *uid = NULL;
@@ -704,8 +702,14 @@ void import_csv(snac *user)
}
else
snac_log(user, xs_fmt("Cannot open file %s", fn));
+}
+
+
+void import_following_accounts_csv(snac *user, const char *fn)
+/* imports a Mastodon CSV file of accounts to follow */
+{
+ FILE *f;
- fn = "following_accounts.csv";
if ((f = fopen(fn, "r")) != NULL) {
snac_log(user, xs_fmt("Importing from %s...", fn));
@@ -757,8 +761,14 @@ void import_csv(snac *user)
}
else
snac_log(user, xs_fmt("Cannot open file %s", fn));
+}
+
+
+void import_list_csv(snac *user, const char *fn)
+/* imports a Mastodon CSV file list */
+{
+ FILE *f;
- fn = "lists.csv";
if ((f = fopen(fn, "r")) != NULL) {
snac_log(user, xs_fmt("Importing from %s...", fn));
@@ -782,6 +792,21 @@ void import_csv(snac *user)
list_content(user, list_id, actor_md5, 1);
snac_log(user, xs_fmt("Added %s to list %s", url, lname));
+
+ if (!following_check(user, url)) {
+ xs *msg = msg_follow(user, url);
+
+ if (msg == NULL) {
+ snac_log(user, xs_fmt("Cannot follow %s -- server down?", acct));
+ continue;
+ }
+
+ following_add(user, url, msg);
+
+ enqueue_output_by_actor(user, msg, url, 0);
+
+ snac_log(user, xs_fmt("Following %s", url));
+ }
}
else
snac_log(user, xs_fmt("Webfinger error while adding %s to list %s", acct, lname));
@@ -793,6 +818,20 @@ void import_csv(snac *user)
}
else
snac_log(user, xs_fmt("Cannot open file %s", fn));
+}
+
+
+void import_csv(snac *user)
+/* import CSV files from Mastodon */
+{
+ FILE *f;
+ const char *fn;
+
+ import_blocked_accounts_csv(user, "blocked_accounts.csv");
+
+ import_following_accounts_csv(user, "following_accounts.csv");
+
+ import_list_csv(user, "lists.csv");
fn = "bookmarks.csv";
if ((f = fopen(fn, "r")) != NULL) {
diff --git a/xs_url.h b/xs_url.h
@@ -106,13 +106,13 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
if (xs_list_len(l1) != 2)
return NULL;
- boundary = xs_dup(xs_list_get(l1, 1));
+ xs *t_boundary = xs_dup(xs_list_get(l1, 1));
/* Tokodon sends the boundary header with double quotes surrounded */
- if (xs_between("\"", boundary, "\"") != 0)
- boundary = xs_strip_chars_i(boundary, "\"");
+ if (xs_between("\"", t_boundary, "\"") != 0)
+ t_boundary = xs_strip_chars_i(t_boundary, "\"");
- boundary = xs_fmt("--%s", boundary);
+ boundary = xs_fmt("--%s", t_boundary);
}
bsz = strlen(boundary);
diff --git a/xs_version.h b/xs_version.h
@@ -1 +1 @@
-/* ab0749f821f1c98d16cbec53201bdf2ba2a24a43 2024-11-20T17:02:42+01:00 */
+/* 297f71e198be7819213e9122e1e78c3b963111bc 2024-11-24T18:48:42+01:00 */