Slon/Modules/ActivityPub: Translate Announce requests to Boosts
When we receive an Announce from someone we are following, we will lookup and/or create the author of the boosted status, followed by the status itself, which will be attached to a new status from the followed user as a "reblog" object. Partially implements #4.
This commit is contained in:
parent
5d7efab319
commit
57ab5d1d1f
2 changed files with 227 additions and 87 deletions
|
@ -1,3 +1,6 @@
|
||||||
|
SlonHttpSession* SLON_AP_DUMMY_SESSION = CAlloc(sizeof(SlonHttpSession));
|
||||||
|
SLON_AP_DUMMY_SESSION->mem_task = slon_mem_task;
|
||||||
|
|
||||||
Bool @slon_activitypub_status_exists(JsonArray* statuses, U8* uri)
|
Bool @slon_activitypub_status_exists(JsonArray* statuses, U8* uri)
|
||||||
{
|
{
|
||||||
if (!statuses || !uri) {
|
if (!statuses || !uri) {
|
||||||
|
@ -666,6 +669,171 @@ JsonObject* @slon_activitypub_status_by_uri(U8* uri, JsonArray* timeline)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JsonObject* @slon_activitypub_create_status_for_remote_account(SlonHttpSession* session, JsonObject* remote_account, JsonObject* o, JsonObject* account = NULL, U8* user = NULL)
|
||||||
|
{
|
||||||
|
if (db->o("statuses")->a(remote_account->@("id"))) {
|
||||||
|
if (@slon_activitypub_status_exists(db->o("statuses")->a(remote_account->@("id")), o->@("atomUri"))) {
|
||||||
|
if (session->status) {
|
||||||
|
// Don't set status if we're using SLON_AP_DUMMY_SESSION
|
||||||
|
session->status(200);
|
||||||
|
}
|
||||||
|
return @slon_api_find_status_by_uri(o->@("atomUri"), remote_account->@("id"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
I64 i;
|
||||||
|
JsonObject* new_status = Json.CreateObject(slon_mem_task);
|
||||||
|
U8* id = @slon_api_generate_unique_id(session);
|
||||||
|
|
||||||
|
JsonArray* media_attachments = Json.CreateArray(slon_mem_task);
|
||||||
|
if (o->@("attachment")) {
|
||||||
|
JsonObject* attachment_item = NULL;
|
||||||
|
JsonObject* media_attachment = NULL;
|
||||||
|
JsonObject* media_meta = NULL;
|
||||||
|
JsonArray* attachment_array = o->@("attachment");
|
||||||
|
for (i = 0; i < attachment_array->length; i++) {
|
||||||
|
attachment_item = attachment_array->o(i);
|
||||||
|
if (attachment_item && attachment_item->@("mediaType") && String.BeginsWith("image", attachment_item->@("mediaType"))) {
|
||||||
|
media_attachment = Json.CreateObject(slon_mem_task);
|
||||||
|
media_meta = Json.CreateObject(slon_mem_task);
|
||||||
|
media_attachment->set("id", "", JSON_STRING);
|
||||||
|
media_attachment->set("type", "image", JSON_STRING);
|
||||||
|
media_attachment->set("url", attachment_item->@("url"), JSON_STRING);
|
||||||
|
media_attachment->set("preview_url", NULL, JSON_NULL);
|
||||||
|
media_attachment->set("remote_url", NULL, JSON_NULL);
|
||||||
|
if (attachment_item->@("width") && attachment_item->@("height")) {
|
||||||
|
media_meta->set("original", Json.CreateObject(slon_mem_task), JSON_OBJECT);
|
||||||
|
media_meta->o("original")->set("width", attachment_item->@("width"), JSON_NUMBER);
|
||||||
|
media_meta->o("original")->set("height", attachment_item->@("height"), JSON_NUMBER);
|
||||||
|
}
|
||||||
|
if (attachment_item->@("summary")) {
|
||||||
|
media_attachment->set("description", attachment_item->@("summary"), JSON_STRING);
|
||||||
|
} else {
|
||||||
|
media_attachment->set("description", NULL, JSON_NULL);
|
||||||
|
}
|
||||||
|
if (attachment_item->@("blurhash")) {
|
||||||
|
media_attachment->set("blurhash", attachment_item->@("blurhash"), JSON_STRING);
|
||||||
|
} else {
|
||||||
|
media_attachment->set("blurhash", NULL, JSON_NULL);
|
||||||
|
}
|
||||||
|
media_attachment->set("meta", media_meta, JSON_OBJECT);
|
||||||
|
media_attachments->append(media_attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account && (o->@("inReplyTo") || o->@("inReplyToAtomUri"))) {
|
||||||
|
JsonObject* reply_to_post = @slon_activitypub_status_by_uri(o->@("inReplyTo"), db->o("timelines")->o("home")->a(account->@("id")));
|
||||||
|
if (reply_to_post) {
|
||||||
|
new_status->set("in_reply_to_id", reply_to_post->@("id"), JSON_STRING);
|
||||||
|
new_status->set("in_reply_to_acct_id", reply_to_post->o("account")->@("id"), JSON_STRING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_status->set("id", id, JSON_STRING);
|
||||||
|
new_status->set("created_at", o->@("published"), JSON_STRING);
|
||||||
|
new_status->set("content", o->@("content"), JSON_STRING);
|
||||||
|
new_status->set("visibility", "public", JSON_STRING);
|
||||||
|
new_status->set("uri", o->@("atomUri"), JSON_STRING);
|
||||||
|
new_status->set("url", o->@("url"), JSON_STRING);
|
||||||
|
new_status->set("account", remote_account, JSON_OBJECT);
|
||||||
|
// new_status->set("application", status_app, JSON_OBJECT);
|
||||||
|
new_status->set("reblogs_count", 0, JSON_NUMBER);
|
||||||
|
new_status->set("favourites_count", 0, JSON_NUMBER);
|
||||||
|
new_status->set("emojis", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||||
|
new_status->set("tags", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||||
|
new_status->set("mentions", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||||
|
new_status->set("media_attachments", media_attachments, JSON_ARRAY);
|
||||||
|
new_status->set("replies_count", 0, JSON_NUMBER);
|
||||||
|
new_status->set("spoiler_text", "", JSON_STRING);
|
||||||
|
new_status->set("sensitive", o->@("sensitive"), JSON_BOOLEAN);
|
||||||
|
|
||||||
|
@slon_api_create_status(new_status, remote_account->@("id"), user);
|
||||||
|
|
||||||
|
@slon_free(session, id);
|
||||||
|
return new_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
U0 @slon_activitypub_reblog_status(SlonHttpSession* session, JsonObject* o, JsonObject* reblogged_status)
|
||||||
|
{
|
||||||
|
JsonObject* remote_account = @slon_api_account_by_remote_actor(o->@("actor_for_key_id"));
|
||||||
|
if (!remote_account) {
|
||||||
|
AdamLog("remote account is NULL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reblogged_status->set("reblogs_count", reblogged_status->@("reblogs_count") + 1, JSON_NUMBER);
|
||||||
|
@slon_db_save_status_to_disk(reblogged_status);
|
||||||
|
|
||||||
|
JsonObject* new_status = Json.CreateObject(slon_mem_task);
|
||||||
|
U8* id = @slon_api_generate_unique_id(session);
|
||||||
|
|
||||||
|
new_status->set("id", id, JSON_STRING);
|
||||||
|
new_status->set("in_reply_to_id", NULL, JSON_NULL);
|
||||||
|
new_status->set("in_reply_to_account_id", NULL, JSON_NULL);
|
||||||
|
new_status->set("content", "", JSON_STRING);
|
||||||
|
new_status->set("created_at", o->o("request")->@("published"), JSON_STRING);
|
||||||
|
new_status->set("visibility", "public", JSON_STRING);
|
||||||
|
new_status->set("uri", reblogged_status->@("uri"), JSON_STRING);
|
||||||
|
new_status->set("url", reblogged_status->@("url"), JSON_STRING);
|
||||||
|
new_status->set("account", remote_account, JSON_OBJECT);
|
||||||
|
// new_status->set("application", status_app, JSON_OBJECT);
|
||||||
|
new_status->set("reblogs_count", 0, JSON_NUMBER);
|
||||||
|
new_status->set("favourites_count", 0, JSON_NUMBER);
|
||||||
|
new_status->set("emojis", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||||
|
new_status->set("tags", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||||
|
new_status->set("mentions", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||||
|
new_status->set("media_attachments", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
||||||
|
new_status->set("replies_count", 0, JSON_NUMBER);
|
||||||
|
new_status->set("spoiler_text", "", JSON_STRING);
|
||||||
|
new_status->set("sensitive", o->@("sensitive"), JSON_BOOLEAN);
|
||||||
|
new_status->set("reblog", reblogged_status, JSON_OBJECT);
|
||||||
|
|
||||||
|
@slon_api_create_status(new_status, remote_account->@("id"), o->@("user"));
|
||||||
|
|
||||||
|
@slon_free(session, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
U0 @slon_activitypub_async_announce_request(JsonObject* o)
|
||||||
|
{
|
||||||
|
Sleep(2000);
|
||||||
|
if (!o) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
U8* reblogged_status_uri = o->o("request")->@("object");
|
||||||
|
U8* reblogged_status_actor = StrNew(reblogged_status_uri, slon_mem_task);
|
||||||
|
StrFind("/statuses/", reblogged_status_actor)[0] = NULL;
|
||||||
|
|
||||||
|
// Does the user exist on this server? If yes, retrieve; if not, fetch and create them
|
||||||
|
JsonObject* reblogged_status_account = @slon_activitypub_get_account_for_remote_actor(SLON_AP_DUMMY_SESSION, reblogged_status_actor);
|
||||||
|
if (!reblogged_status_account) {
|
||||||
|
// FIXME: We probably should handle this, or not.. it will just stay queued and retry until we Accept
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the status to be reblogged
|
||||||
|
U8* fetch_buffer = CAlloc(HTTP_FETCH_BUFFER_SIZE, slon_mem_task);
|
||||||
|
U8* signatory = db->o("actors")->o(o->@("user"))->@("id");
|
||||||
|
@http_response* resp = @slon_activitypub_signed_request(reblogged_status_uri, fetch_buffer, NULL, SLON_HTTP_VERB_GET, signatory);
|
||||||
|
|
||||||
|
// AdamLog("code: %d\n", resp->status.code);
|
||||||
|
|
||||||
|
// Create the status if it does not already exist on this server
|
||||||
|
JsonObject* remote_status = Json.Parse(resp->body.data, slon_db_mem_task);
|
||||||
|
JsonObject* reblog_status = @slon_activitypub_create_status_for_remote_account(SLON_AP_DUMMY_SESSION, reblogged_status_account, remote_status);
|
||||||
|
|
||||||
|
AdamLog("reblog_status: %s\n", Json.Stringify(reblog_status, slon_mem_task));
|
||||||
|
|
||||||
|
// Create the reblog
|
||||||
|
@slon_activitypub_reblog_status(SLON_AP_DUMMY_SESSION, o, reblog_status);
|
||||||
|
|
||||||
|
// Send the Accept request
|
||||||
|
Spawn(&@slon_activitypub_async_accept_request, o, "SlonAsyncAcceptTask");
|
||||||
|
|
||||||
|
Free(fetch_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
U0 @slon_activitypub_users_inbox(SlonHttpSession* session, U8* user)
|
U0 @slon_activitypub_users_inbox(SlonHttpSession* session, U8* user)
|
||||||
{
|
{
|
||||||
SLON_SCRATCH_BUFFER_AND_REQUEST_JSON
|
SLON_SCRATCH_BUFFER_AND_REQUEST_JSON
|
||||||
|
@ -684,22 +852,18 @@ U0 @slon_activitypub_users_inbox(SlonHttpSession* session, U8* user)
|
||||||
U8* status_id = NULL;
|
U8* status_id = NULL;
|
||||||
|
|
||||||
if (!StrICmp("announce", request_json->@("type"))) {
|
if (!StrICmp("announce", request_json->@("type"))) {
|
||||||
if (StrICmp(session->actor_for_key_id, request_json->@("actor"))) {
|
if (StrICmp(session->actor_for_key_id, request_json->@("actor")) || !@slon_api_user_is_following(user, session->actor_for_key_id)) {
|
||||||
session->status(401);
|
session->status(401);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
status_id = StrFind("/", StrFind("/statuses/", request_json->@("object")) + 1) + 1;
|
|
||||||
statuses = db->o("statuses")->a(account->@("id"));
|
JsonObject* announce = Json.CreateObject(slon_mem_task);
|
||||||
for (i = 0; i < statuses->length; i++) {
|
announce->set("actor_for_key_id", session->actor_for_key_id, JSON_STRING);
|
||||||
status = statuses->@(i);
|
announce->set("user", user, JSON_STRING);
|
||||||
if (!StrICmp(status_id, status->@("id"))) {
|
announce->set("request", Json.Clone(request_json, slon_mem_task), JSON_OBJECT);
|
||||||
// TODO: https://docs.joinmastodon.org/methods/statuses/#reblogged_by
|
Spawn(&@slon_activitypub_async_announce_request, announce, "SlonAsyncAnnounceTask");
|
||||||
status->set("reblog_count", status->@("reblog_count") + 1);
|
session->status(200);
|
||||||
break;
|
return;
|
||||||
}
|
|
||||||
}
|
|
||||||
@slon_db_save_statuses_to_disk;
|
|
||||||
request_object = Json.Clone(request_json, slon_mem_task);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StrICmp("follow", request_json->@("type"))) {
|
if (!StrICmp("follow", request_json->@("type"))) {
|
||||||
|
@ -798,87 +962,15 @@ U0 @slon_activitypub_users_inbox(SlonHttpSession* session, U8* user)
|
||||||
session->status(401);
|
session->status(401);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonObject* remote_account = @slon_activitypub_get_account_for_remote_actor(session, request_json->@("actor"));
|
JsonObject* remote_account = @slon_activitypub_get_account_for_remote_actor(session, request_json->@("actor"));
|
||||||
if (!remote_account) {
|
if (!remote_account) {
|
||||||
session->status(500);
|
session->status(500);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (db->o("statuses")->a(remote_account->@("id"))) {
|
|
||||||
if (@slon_activitypub_status_exists(db->o("statuses")->a(remote_account->@("id")), request_json->o("object")->@("atomUri"))) {
|
|
||||||
session->status(200);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonObject* new_status = Json.CreateObject(slon_mem_task);
|
@slon_activitypub_create_status_for_remote_account(session, remote_account, request_json->o("object"), account, user);
|
||||||
U8* id = @slon_api_generate_unique_id(session);
|
|
||||||
|
|
||||||
JsonArray* media_attachments = Json.CreateArray(slon_mem_task);
|
|
||||||
if (request_json->o("object")->@("attachment")) {
|
|
||||||
JsonObject* attachment_item = NULL;
|
|
||||||
JsonObject* media_attachment = NULL;
|
|
||||||
JsonObject* media_meta = NULL;
|
|
||||||
JsonArray* attachment_array = request_json->o("object")->@("attachment");
|
|
||||||
for (i = 0; i < attachment_array->length; i++) {
|
|
||||||
attachment_item = attachment_array->o(i);
|
|
||||||
if (attachment_item && attachment_item->@("mediaType") && String.BeginsWith("image", attachment_item->@("mediaType"))) {
|
|
||||||
media_attachment = Json.CreateObject(slon_mem_task);
|
|
||||||
media_meta = Json.CreateObject(slon_mem_task);
|
|
||||||
media_attachment->set("id", "", JSON_STRING);
|
|
||||||
media_attachment->set("type", "image", JSON_STRING);
|
|
||||||
media_attachment->set("url", attachment_item->@("url"), JSON_STRING);
|
|
||||||
media_attachment->set("preview_url", NULL, JSON_NULL);
|
|
||||||
media_attachment->set("remote_url", NULL, JSON_NULL);
|
|
||||||
if (attachment_item->@("width") && attachment_item->@("height")) {
|
|
||||||
media_meta->set("original", Json.CreateObject(slon_mem_task), JSON_OBJECT);
|
|
||||||
media_meta->o("original")->set("width", attachment_item->@("width"), JSON_NUMBER);
|
|
||||||
media_meta->o("original")->set("height", attachment_item->@("height"), JSON_NUMBER);
|
|
||||||
}
|
|
||||||
if (attachment_item->@("summary")) {
|
|
||||||
media_attachment->set("description", attachment_item->@("summary"), JSON_STRING);
|
|
||||||
} else {
|
|
||||||
media_attachment->set("description", NULL, JSON_NULL);
|
|
||||||
}
|
|
||||||
if (attachment_item->@("blurhash")) {
|
|
||||||
media_attachment->set("blurhash", attachment_item->@("blurhash"), JSON_STRING);
|
|
||||||
} else {
|
|
||||||
media_attachment->set("blurhash", NULL, JSON_NULL);
|
|
||||||
}
|
|
||||||
media_attachment->set("meta", media_meta, JSON_OBJECT);
|
|
||||||
media_attachments->append(media_attachment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request_json->o("object")->@("inReplyTo") || request_json->o("object")->@("inReplyToAtomUri")) {
|
|
||||||
JsonObject* reply_to_post = @slon_activitypub_status_by_uri(request_json->o("object")->@("inReplyTo"), db->o("timelines")->o("home")->a(account->@("id")));
|
|
||||||
if (reply_to_post) {
|
|
||||||
new_status->set("in_reply_to_id", reply_to_post->@("id"), JSON_STRING);
|
|
||||||
new_status->set("in_reply_to_acct_id", reply_to_post->o("account")->@("id"), JSON_STRING);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new_status->set("id", id, JSON_STRING);
|
|
||||||
new_status->set("created_at", request_json->@("published"), JSON_STRING);
|
|
||||||
new_status->set("content", request_json->o("object")->@("content"), JSON_STRING);
|
|
||||||
new_status->set("visibility", "public", JSON_STRING);
|
|
||||||
new_status->set("uri", request_json->o("object")->@("atomUri"), JSON_STRING);
|
|
||||||
new_status->set("url", request_json->o("object")->@("url"), JSON_STRING);
|
|
||||||
new_status->set("account", remote_account, JSON_OBJECT);
|
|
||||||
// new_status->set("application", status_app, JSON_OBJECT);
|
|
||||||
new_status->set("reblogs_count", 0, JSON_NUMBER);
|
|
||||||
new_status->set("favourites_count", 0, JSON_NUMBER);
|
|
||||||
new_status->set("emojis", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
|
||||||
new_status->set("tags", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
|
||||||
new_status->set("mentions", SLON_EMPTY_JSON_ARRAY, JSON_ARRAY);
|
|
||||||
new_status->set("media_attachments", media_attachments, JSON_ARRAY);
|
|
||||||
new_status->set("replies_count", 0, JSON_NUMBER);
|
|
||||||
new_status->set("spoiler_text", "", JSON_STRING);
|
|
||||||
new_status->set("sensitive", request_json->o("object")->@("sensitive"), JSON_BOOLEAN);
|
|
||||||
|
|
||||||
@slon_api_create_status(new_status, remote_account->@("id"), user);
|
|
||||||
|
|
||||||
@slon_free(session, id);
|
|
||||||
request_object = Json.CreateObject(slon_mem_task);
|
request_object = Json.CreateObject(slon_mem_task);
|
||||||
request_object->set("@context", "https://www.w3.org/ns/activitystreams", JSON_STRING);
|
request_object->set("@context", "https://www.w3.org/ns/activitystreams", JSON_STRING);
|
||||||
request_object->set("id", request_json->@("id"), JSON_STRING);
|
request_object->set("id", request_json->@("id"), JSON_STRING);
|
||||||
|
|
|
@ -477,6 +477,22 @@ JsonObject* @slon_api_status_lookup_by_in_reply_to_id(U8* id, JsonArray* statuse
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JsonObject* @slon_api_status_lookup_by_uri(U8* uri, JsonArray* statuses)
|
||||||
|
{
|
||||||
|
if (!uri || !statuses) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
I64 i;
|
||||||
|
JsonObject* status;
|
||||||
|
for (i = 0; i < statuses->length; i++) {
|
||||||
|
status = statuses->@(i);
|
||||||
|
if (!status->@("deleted") && status->@("uri") && !StrICmp(status->@("uri"), uri)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
JsonObject* @slon_api_find_status_by_id(U8* id, U8* account_id = NULL)
|
JsonObject* @slon_api_find_status_by_id(U8* id, U8* account_id = NULL)
|
||||||
{
|
{
|
||||||
if (account_id) {
|
if (account_id) {
|
||||||
|
@ -494,6 +510,23 @@ JsonObject* @slon_api_find_status_by_id(U8* id, U8* account_id = NULL)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JsonObject* @slon_api_find_status_by_uri(U8* uri, U8* account_id = NULL)
|
||||||
|
{
|
||||||
|
if (account_id) {
|
||||||
|
return @slon_api_status_lookup_by_uri(uri, db->o("statuses")->a(account_id));
|
||||||
|
}
|
||||||
|
JsonObject* status = NULL;
|
||||||
|
JsonKey* key = db->o("statuses")->keys;
|
||||||
|
while (key) {
|
||||||
|
status = @slon_api_status_lookup_by_uri(uri, key->value);
|
||||||
|
if (status) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
key = key->next;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
U0 @slon_api_create_status(JsonObject* status, U8* account_id, U8* to_ap_user = NULL)
|
U0 @slon_api_create_status(JsonObject* status, U8* account_id, U8* to_ap_user = NULL)
|
||||||
{
|
{
|
||||||
if (!status || !account_id) {
|
if (!status || !account_id) {
|
||||||
|
@ -585,3 +618,18 @@ Bool @slon_api_get_value_as_boolean(JsonKey* key)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool @slon_api_user_is_following(U8* user, U8* actor)
|
||||||
|
{
|
||||||
|
JsonArray* iter_following = db->o("following")->a(user);
|
||||||
|
if (!iter_following) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
I64 i;
|
||||||
|
for (i = 0; i < iter_following->length; i++) {
|
||||||
|
if (!StrICmp(actor, iter_following->@(i))) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue