summaryrefslogtreecommitdiff
path: root/libgs/src
diff options
context:
space:
mode:
Diffstat (limited to 'libgs/src')
-rw-r--r--libgs/src/account.c93
-rw-r--r--libgs/src/account.h30
-rw-r--r--libgs/src/app.c115
-rw-r--r--libgs/src/app.h13
-rw-r--r--libgs/src/auth.c109
-rw-r--r--libgs/src/auth.h12
-rw-r--r--libgs/src/client.c72
-rw-r--r--libgs/src/client.h29
-rw-r--r--libgs/src/config.c174
-rw-r--r--libgs/src/config.h28
-rw-r--r--libgs/src/gs.c13
-rw-r--r--libgs/src/gs.h17
-rw-r--r--libgs/src/http.c178
-rw-r--r--libgs/src/http.h9
-rw-r--r--libgs/src/instance_info.c38
-rw-r--r--libgs/src/instance_info.h31
-rw-r--r--libgs/src/log.c30
-rw-r--r--libgs/src/log.h13
-rw-r--r--libgs/src/status.c120
-rw-r--r--libgs/src/status.h31
-rw-r--r--libgs/src/string-util.c86
-rw-r--r--libgs/src/string-util.h10
-rw-r--r--libgs/src/timeline.c93
-rw-r--r--libgs/src/timeline.h17
24 files changed, 1361 insertions, 0 deletions
diff --git a/libgs/src/account.c b/libgs/src/account.c
new file mode 100644
index 0000000..ecf3c28
--- /dev/null
+++ b/libgs/src/account.c
@@ -0,0 +1,93 @@
+#define _POSIX_C_SOURCE 200809L
+#include <err.h>
+#include <string.h>
+#include <jansson.h>
+#include "account.h"
+#include "log.h"
+
+struct gs_account *gs_account_from_json(const char *json_data)
+{
+ struct gs_account *a;
+ json_t *root;
+ json_error_t error;
+
+ root = json_loads(json_data, 0, &error);
+ if (!root) {
+ gs_log(GS_WARNING, "gs_account_from_json",
+ "json parse error: line %d: %s", error.line, error.text);
+ return NULL;
+ }
+
+ a = gs_account_from_json_t(root);
+ json_decref(root);
+ return a;
+}
+
+struct gs_account *gs_account_from_json_t(const json_t *root)
+{
+ struct gs_account *a;
+ if (!root) {
+ gs_log(GS_WARNING, "gs_account_from_json_t", "json data is null");
+ return NULL;
+ }
+
+ if (!json_is_object(root)) {
+ gs_log(GS_WARNING, "gs_account_from_json_t",
+ "json root is not object");
+ return NULL;
+ }
+
+ a = calloc(1, sizeof(struct gs_account));
+ if (!a) {
+ err(1, "gs_account_from_json_t");
+ return NULL;
+ }
+
+ json_t *id = json_object_get(root, "id");
+ if (json_is_string(id)) {
+ a->id = strdup(json_string_value(id));
+ }
+
+ json_t *username = json_object_get(root, "username");
+ if (json_is_string(username)) {
+ a->username = strdup(json_string_value(username));
+ }
+
+ json_t *acct = json_object_get(root, "acct");
+ if (json_is_string(acct)) {
+ a->acct = strdup(json_string_value(acct));
+ }
+
+ json_t *display_name = json_object_get(root, "display_name");
+ if (json_is_string(display_name)) {
+ a->display_name = strdup(json_string_value(display_name));
+ }
+
+ return a;
+}
+
+void gs_account_free(struct gs_account *a)
+{
+ if (a->id)
+ free(a->id);
+ if (a->username)
+ free(a->username);
+ if (a->acct)
+ free(a->acct);
+ if (a->display_name)
+ free(a->display_name);
+ if (a->note)
+ free(a->note);
+ if (a->url)
+ free(a->url);
+ if (a->avatar)
+ free(a->avatar);
+ if (a->avatar_static)
+ free(a->avatar_static);
+ if (a->header)
+ free(a->header);
+ if (a->header_static)
+ free(a->header_static);
+ free(a);
+}
+
diff --git a/libgs/src/account.h b/libgs/src/account.h
new file mode 100644
index 0000000..c9d36d2
--- /dev/null
+++ b/libgs/src/account.h
@@ -0,0 +1,30 @@
+#ifndef __GS_ACCOUNT_H
+#define __GS_ACCOUNT_H
+
+#include <stdbool.h>
+#include <jansson.h>
+
+struct gs_account {
+ char *id;
+ char *username;
+ char *acct;
+ char *display_name;
+ bool locked;
+ unsigned int follower_count;
+ unsigned int following_count;
+ unsigned int statuses_count;
+ char *note;
+ char *url;
+ char *avatar;
+ char *avatar_static;
+ char *header;
+ char *header_static;
+ struct gs_account *moved;
+ bool bot;
+};
+
+struct gs_account *gs_account_from_json(const char *json_data);
+struct gs_account *gs_account_from_json_t(const json_t *);
+void gs_account_free(struct gs_account *);
+
+#endif
diff --git a/libgs/src/app.c b/libgs/src/app.c
new file mode 100644
index 0000000..b3f6e20
--- /dev/null
+++ b/libgs/src/app.c
@@ -0,0 +1,115 @@
+#define _POSIX_C_SOURCE 200809L
+#include <err.h>
+#include <jansson.h>
+#include <string.h>
+#include "app.h"
+#include "http.h"
+#include "log.h"
+
+#define CLIENT_NAME "ap_client"
+#define REGISTER_URL "https://%s/api/v1/apps"
+
+static char *get_register_url(const char *domain)
+{
+ char *url;
+ size_t size;
+
+ size = strlen(REGISTER_URL) + strlen(domain) - 1;
+ url = malloc(size);
+ if (!url) {
+ err(1, NULL);
+ return NULL;
+ }
+
+ sprintf(url, REGISTER_URL, domain);
+ return url;
+}
+
+static char *get_register_req()
+{
+ char *req;
+ json_t *json_root;
+
+ json_root = json_pack_ex(NULL, 1, "{s:s, s:s, s:s}", "client_name",
+ CLIENT_NAME, "redirect_uris", "urn:ietf:wg:oauth:2.0:oob", "scopes",
+ "read write push");
+ if (!json_root) {
+ return NULL;
+ }
+
+ req = json_dumps(json_root, 0);
+ json_decref(json_root);
+
+ return req;
+}
+
+static struct gs_app *get_register_resp(const char *data)
+{
+ struct gs_app *resp;
+ json_t *json_root;
+ json_t *cid, *csec;
+
+ json_root = json_loads(data, 0, NULL);
+ if (!json_root) {
+ return NULL;
+ }
+
+ if (!json_is_object(json_root)) {
+ json_decref(json_root);
+ return NULL;
+ }
+
+ cid = json_object_get(json_root, "client_id");
+ csec = json_object_get(json_root, "client_secret");
+ if (!json_is_string(cid) || !json_is_string(csec)) {
+ json_decref(json_root);
+ return NULL;
+ }
+
+ resp = calloc(1, sizeof(struct gs_app));
+ resp->client_id = strdup(json_string_value(cid));
+ resp->client_secret = strdup(json_string_value(csec));
+
+ json_decref(json_root);
+ return resp;
+}
+
+struct gs_app *gs_app_register(const char *domain)
+{
+ char *url, *req_data, *resp_data;
+ struct gs_app *resp;
+
+ if (!domain || !*domain) {
+ gs_log(GS_WARNING, "gs_app_register", "invalid argument");
+ return NULL;
+ }
+
+ url = get_register_url(domain);
+ if (!url) {
+ gs_log(GS_WARNING, "gs_app_register", "invalid argument");
+ return NULL;
+ }
+
+ req_data = get_register_req();
+ if (!req_data) {
+ free(url);
+ return NULL;
+ }
+
+ resp_data = gs_http_post(url, NULL, req_data);
+ free(url);
+ free(req_data);
+ if (!resp_data) {
+ gs_log(GS_WARNING, "gs_app_register", "register request failed");
+ return NULL;
+ }
+
+ resp = get_register_resp(resp_data);
+ free(resp_data);
+ if (!resp) {
+ gs_log(GS_WARNING, "gs_app_register", "invalid response");
+ return NULL;
+ }
+
+ return resp;
+}
diff --git a/libgs/src/app.h b/libgs/src/app.h
new file mode 100644
index 0000000..f7b6e8a
--- /dev/null
+++ b/libgs/src/app.h
@@ -0,0 +1,13 @@
+#ifndef __GS_APP_H
+#define __GS_APP_H
+
+struct gs_app {
+ char *client_id;
+ char *client_secret;
+};
+
+struct gs_app *gs_app_register(const char *domain);
+
+#endif
+
+
diff --git a/libgs/src/auth.c b/libgs/src/auth.c
new file mode 100644
index 0000000..88ab7b7
--- /dev/null
+++ b/libgs/src/auth.c
@@ -0,0 +1,109 @@
+#define _POSIX_C_SOURCE 200809L
+#include <err.h>
+#include <string.h>
+#include <jansson.h>
+#include "auth.h"
+#include "client.h"
+#include "log.h"
+#include "string-util.h"
+
+struct login_response {
+ char *access_token;
+ char *scope;
+};
+
+static char *get_login_req(const char *client_id, const char *client_secret,
+ const char *email, const char *password)
+{
+ char *req;
+ json_t *json_root;
+ json_error_t error;
+
+ json_root = json_pack_ex(&error, 1, "{s:s, s:s, s:s, s:s, s:s}",
+ "client_id", client_id, "client_secret",
+ client_secret, "grant_type", "password", "username", email,
+ "password", password);
+ if (!json_root) {
+ return NULL;
+ }
+
+ req = json_dumps(json_root, 0);
+ json_decref(json_root);
+
+ return req;
+}
+
+static struct login_response *get_login_resp(char *data)
+{
+ struct login_response *resp;
+ json_t *json_root;
+ json_t *access_token, *scope;
+
+ json_root = json_loads(data, 0, NULL);
+ if (!json_root) {
+ return NULL;
+ }
+
+ if (!json_is_object(json_root)) {
+ json_decref(json_root);
+ return NULL;
+ }
+
+ access_token = json_object_get(json_root, "access_token");
+ scope = json_object_get(json_root, "scope");
+ if (!json_is_string(access_token) || !json_is_string(scope)) {
+ json_decref(json_root);
+ return NULL;
+ }
+
+ resp = calloc(1, sizeof(struct login_response));
+ resp->access_token = strdup(json_string_value(access_token));
+ resp->scope = strdup(json_string_value(scope));
+
+ json_decref(json_root);
+ return resp;
+}
+
+int gs_auth_authenticate(GSClient *c, const char *email, const char *password)
+{
+ char *req_data, *resp_data;
+ struct login_response *resp;
+
+ if (!email || !*email || !password || !*password) {
+ gs_log(GS_WARNING, "gs_auth_authenticate", "invalid argument");
+ return -1;
+ }
+
+ req_data = get_login_req(c->client_id, c->client_secret, email, password);
+ if (!req_data) {
+ gs_log(GS_WARNING, "gs_auth_authenticate", "invalid argument");
+ return -1;
+ }
+
+ resp_data = (char *)gs_client_do_api(c, 2, "/oauth/token", req_data);
+ free(req_data);
+ if (!resp_data) {
+ gs_log(GS_WARNING, "gs_auth_authenticate", "invalid response");
+ return -1;
+ }
+
+ resp = get_login_resp(resp_data);
+ free(resp_data);
+ if (!resp) {
+ gs_log(GS_WARNING, "gs_auth_authenticate", "invalid response");
+ return -1;
+ }
+
+ gs_client_set_token(c, resp->access_token);
+
+ free(resp->access_token);
+ free(resp->scope);
+ free(resp);
+
+ return 0;
+}
+
+bool gs_auth_is_logged_in(GSClient *c)
+{
+ return c->access_token != NULL;
+}
diff --git a/libgs/src/auth.h b/libgs/src/auth.h
new file mode 100644
index 0000000..431e0d7
--- /dev/null
+++ b/libgs/src/auth.h
@@ -0,0 +1,12 @@
+#ifndef __GS_AUTH_H
+#define __GS_AUTH_H
+
+#include <stdbool.h>
+#include "client.h"
+
+int gs_auth_authenticate(GSClient *c, const char *email, const char *password);
+bool gs_auth_is_logged_in(GSClient *c);
+
+
+#endif
+
diff --git a/libgs/src/client.c b/libgs/src/client.c
new file mode 100644
index 0000000..7c0b62c
--- /dev/null
+++ b/libgs/src/client.c
@@ -0,0 +1,72 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include <err.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include "client.h"
+#include "http.h"
+#include "log.h"
+#include "string-util.h"
+
+GSClient *gs_client_new(const char *server, const char *client_id,
+ const char *client_secret)
+{
+ GSClient *c;
+
+ c = calloc(1, sizeof(GSClient));
+ if (!c) {
+ err(1, "gs_new_client");
+ return NULL;
+ }
+
+ c->server = strdup(server);
+ c->client_id = strdup(client_id);
+ c->client_secret = strdup(client_secret);
+
+ return c;
+}
+
+void *gs_client_do_api(GSClient *c, int method, const char *url,
+ const char *req)
+{
+ char *resp;
+
+ size_t s = strlen(c->server) + strlen(url) + 1;
+ char full_url[s];
+ strlcpy(full_url, c->server, s);
+ strlcat(full_url, url, s);
+
+ switch (method) {
+ case 1:
+ resp = gs_http_get(full_url, c->access_token);
+ break;
+ case 2:
+ resp = gs_http_post(full_url, c->access_token, req);
+ break;
+ default:
+ gs_log(GS_WARNING, "gs_client_do_api", "invalid api method");
+ return NULL;
+ }
+
+ return resp;
+}
+
+void gs_client_set_token(GSClient *c, const char *access_token)
+{
+ c->access_token = strdup(access_token);
+}
+
+void gs_client_free(GSClient *c)
+{
+ if (!c)
+ return;
+ if (c->server)
+ free(c->server);
+ if (c->client_id)
+ free(c->client_id);
+ if (c->client_secret)
+ free(c->client_secret);
+ if (c->access_token)
+ free(c->access_token);
+}
diff --git a/libgs/src/client.h b/libgs/src/client.h
new file mode 100644
index 0000000..d24b1ff
--- /dev/null
+++ b/libgs/src/client.h
@@ -0,0 +1,29 @@
+#ifndef __GS_CLIENT_H
+#define __GS_CLIENT_H
+
+typedef struct {
+ char *server;
+ char *client_id;
+ char *client_secret;
+ char *access_token;
+} GSClient;
+
+/*
+ * Create new GSClient with server url, client_id and client_secret returned
+ * from gs_register call.
+ * It will add `http://` prefix to server if it doesn't exists.
+ */
+GSClient *gs_client_new(const char *server, const char *client_id,
+ const char *client_secret);
+
+/* Generic helper fuction to make http calls */
+void *gs_client_do_api(GSClient *c, int method, const char *url,
+ const char *req);
+
+/* Set user access_token */
+void gs_client_set_token(GSClient *c, const char *access_token);
+
+/* Free GSClient */
+void gs_client_free(GSClient *c);
+
+#endif
diff --git a/libgs/src/config.c b/libgs/src/config.c
new file mode 100644
index 0000000..167c9af
--- /dev/null
+++ b/libgs/src/config.c
@@ -0,0 +1,174 @@
+#define _POSIX_C_SOURCE 200809L
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <err.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <limits.h>
+#include <libgen.h>
+#include <jansson.h>
+#include "string-util.h"
+#include "config.h"
+#include "log.h"
+
+#define CONFIG_PATH_SUFFIX "/.config/ap_client/config"
+
+struct config _config;
+const struct config *config = (const struct config *)&_config;
+
+bool is_logged_in()
+{
+ if (!(config->access_token) || !*(config->access_token) ||
+ !(config->instance_url) || !*(config->instance_url)) {
+ return false;
+ }
+ return true;
+}
+
+const char *get_access_token()
+{
+ return (const char *)config->access_token;
+}
+
+const char *get_instance_url()
+{
+ return (const char *)config->instance_url;
+}
+
+bool is_registered()
+{
+ if (!(_config.client_id) || !*(_config.client_id) ||
+ !(_config.client_secret) || !*(_config.client_secret) ||
+ !(_config.instance_url) || !*(_config.instance_url)) {
+ return false;
+ }
+ return true;
+}
+
+char *get_config_path()
+{
+ char *home = getenv("HOME");
+ if (!home || !*home) {
+ return NULL;
+ }
+ char config_path[PATH_MAX];
+ strlcpy(config_path, home, sizeof(config_path));
+ strlcat(config_path, CONFIG_PATH_SUFFIX, sizeof(config_path));
+ return strdup(config_path);
+}
+
+void config_load()
+{
+ json_t *root;
+ json_error_t error;
+
+ char *path = get_config_path();
+ root = json_load_file(path, 0, &error);
+ if (!root) {
+ gs_log(GS_WARNING, "config_load", "json parse error: line %d: %s",
+ error.line, error.text);
+ free(path);
+ return;
+ }
+ free(path);
+
+ if (!json_is_object(root)) {
+ gs_log(GS_WARNING, "config_load", "json root is not object");
+ json_decref(root);
+ return;
+ }
+
+ json_t *instance_url = json_object_get(root, "instance_url");
+ if (json_is_string(instance_url)) {
+ _config.instance_url = strdup(json_string_value(instance_url));
+ }
+
+ json_t *client_id = json_object_get(root, "client_id");
+ if (json_is_string(client_id)) {
+ _config.client_id = strdup(json_string_value(client_id));
+ }
+
+ json_t *client_secret = json_object_get(root, "client_secret");
+ if (json_is_string(client_secret)) {
+ _config.client_secret = strdup(json_string_value(client_secret));
+ }
+
+ json_t *access_token = json_object_get(root, "access_token");
+ if (json_is_string(access_token)) {
+ _config.access_token = strdup(json_string_value(access_token));
+ }
+
+ json_decref(root);
+}
+
+int config_save()
+{
+ json_t *root;
+ json_error_t error;
+
+ root = json_pack_ex(&error, 1, "{s:s, s:s, s:s, s:s}", "instance_url",
+ _config.instance_url, "client_id", _config.client_id,
+ "client_secret", _config.client_secret, "access_token",
+ _config.access_token);
+
+ if (!root) {
+ gs_log(GS_WARNING, "register_app", "json pack error: line %d: %s",
+ error.line, error.text);
+ return -1;
+ }
+
+ char *config_path = get_config_path();
+ struct stat st;
+ char *dir = dirname(config_path);
+ if (stat(dir, &st)) {
+ if (mkdir(dir, S_IRWXU)) {
+ free(config_path);
+ gs_log(GS_WARNING, "config_save", "failed to create config dir");
+ return -1;
+ }
+ }
+ free(config_path);
+
+ if (json_dump_file(root, get_config_path(), 0)) {
+ gs_log(GS_WARNING, "config_save", "unable to save json");
+ json_decref(root);
+ return -1;
+ }
+
+ json_decref(root);
+ return 0;
+}
+
+void config_set_client_id(const char *cid)
+{
+ _config.client_id = strdup(cid);
+}
+
+void config_set_client_secret(const char *cs)
+{
+ _config.client_secret = strdup(cs);
+}
+
+void config_set_instance_url(const char *iu)
+{
+ _config.instance_url = strdup(iu);
+}
+
+void config_set_access_token(const char *a)
+{
+ _config.access_token = strdup(a);
+}
+
+void config_cleanup()
+{
+ if (_config.client_id)
+ free(_config.client_id);
+ if (_config.client_secret)
+ free(_config.client_secret);
+ if (_config.instance_url)
+ free(_config.instance_url);
+ if (_config.access_token)
+ free(_config.access_token);
+}
diff --git a/libgs/src/config.h b/libgs/src/config.h
new file mode 100644
index 0000000..dd1f38d
--- /dev/null
+++ b/libgs/src/config.h
@@ -0,0 +1,28 @@
+#ifndef __GS_CONFIG_H
+#define __GS_CONFIG_H
+
+#include <stdbool.h>
+
+struct gs_config {
+ char *instance_url;
+ char *client_id;
+ char *client_secret;
+ char *access_token;
+};
+
+extern const struct gs_config *config;
+
+bool gs_is_logged_in();
+const char* gs_get_access_token();
+const char* gs_get_instance_url();
+int gs_read_local_credentials();
+bool gs_is_registered();
+void gs_config_load();
+int gs_config_save();
+void gs_config_set_client_id(const char *cid);
+void gs_config_set_client_secret(const char *cs);
+void gs_config_set_instance_url(const char *iu);
+void gs_config_set_access_token(const char *a);
+void gs_config_cleanup();
+
+#endif
diff --git a/libgs/src/gs.c b/libgs/src/gs.c
new file mode 100644
index 0000000..587de59
--- /dev/null
+++ b/libgs/src/gs.c
@@ -0,0 +1,13 @@
+#define _POSIX_C_SOURCE 200809L
+#include "gs.h"
+#include "http.h"
+
+int gs_init()
+{
+ return gs_http_init();
+}
+
+void gs_cleanup()
+{
+ gs_http_cleanup();
+}
diff --git a/libgs/src/gs.h b/libgs/src/gs.h
new file mode 100644
index 0000000..c69ca40
--- /dev/null
+++ b/libgs/src/gs.h
@@ -0,0 +1,17 @@
+#ifndef __GS_H
+#define __GS_H
+
+#include "app.h"
+#include "account.h"
+#include "auth.h"
+#include "client.h"
+#include "http.h"
+#include "log.h"
+#include "string-util.h"
+#include "status.h"
+#include "timeline.h"
+
+int gs_init();
+void gs_cleanup();
+
+#endif
diff --git a/libgs/src/http.c b/libgs/src/http.c
new file mode 100644
index 0000000..e1224ff
--- /dev/null
+++ b/libgs/src/http.c
@@ -0,0 +1,178 @@
+#define _POSIX_C_SOURCE 200809L
+#include <stdlib.h>
+#include <string.h>
+#include <pthread.h>
+#include <err.h>
+#include <curl/curl.h>
+#include "string-util.h"
+#include "auth.h"
+#include "client.h"
+#include "log.h"
+
+#define BUFFER_SIZE (256 * 1024)
+#define AUTH_HEADER_STR_PREFIX "Authorization: Bearer "
+
+int gs_http_init()
+{
+ if (curl_global_init(CURL_GLOBAL_ALL)) {
+ gs_log(GS_WARNING, "http_init", "failed to init curl");
+ return -1;
+ }
+ return 0;
+}
+
+void gs_http_cleanup()
+{
+ curl_global_cleanup();
+ return;
+}
+
+struct write_result {
+ char *data;
+ int pos;
+};
+
+static size_t write_response(void *ptr, size_t size, size_t nmemb, void *stream)
+{
+ struct write_result *result = (struct write_result *)stream;
+
+ if (result->pos + size * nmemb >= BUFFER_SIZE - 1) {
+ gs_log(GS_WARNING, "write_response", "buffer too small");
+ return 0;
+ }
+
+ memcpy(result->data + result->pos, ptr, size * nmemb);
+ result->pos += size * nmemb;
+
+ return size * nmemb;
+}
+
+char *gs_http_get(const char *url, const char *token)
+{
+ CURL *curl = NULL;
+ CURLcode status;
+ struct curl_slist *headers = NULL;
+ char *data = NULL;
+ long code;
+
+ curl = curl_easy_init();
+ if (!curl)
+ goto error;
+
+ data = malloc(BUFFER_SIZE);
+ if (!data)
+ goto error;
+
+ struct write_result write_result = {.data = data, .pos = 0};
+
+ if (token) {
+ size_t s = strlen(AUTH_HEADER_STR_PREFIX) + strlen(token) + 1;
+ char auth_header_val[s];
+ strlcpy(auth_header_val, AUTH_HEADER_STR_PREFIX, s);
+ strlcat(auth_header_val, token, s);
+ headers = curl_slist_append(headers, auth_header_val);
+ }
+
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_response);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &write_result);
+
+ status = curl_easy_perform(curl);
+ if (status) {
+ gs_log(GS_WARNING, "get_request",
+ "unable to request data from %s: %s", url,
+ curl_easy_strerror(status));
+ goto error;
+ }
+
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
+ if (code != 200) {
+ gs_log(GS_WARNING, "get_request", "server responded with code %ld",
+ code);
+ goto error;
+ }
+
+ data[write_result.pos] = '\0';
+
+ curl_slist_free_all(headers);
+ curl_easy_cleanup(curl);
+ return data;
+
+error:
+ if (data)
+ free(data);
+ if (headers)
+ curl_slist_free_all(headers);
+ if (curl)
+ curl_easy_cleanup(curl);
+ return NULL;
+}
+
+char *gs_http_post(const char *url, const char *token, const char *post_data)
+{
+ CURL *curl = NULL;
+ CURLcode status;
+ struct curl_slist *headers = NULL;
+ char *data = NULL;
+ long code;
+
+ curl = curl_easy_init();
+ if (!curl)
+ goto error;
+
+ data = malloc(BUFFER_SIZE);
+ if (!data)
+ goto error;
+
+ struct write_result write_result = {.data = data, .pos = 0};
+
+ if (token) {
+ size_t s = strlen(AUTH_HEADER_STR_PREFIX) + strlen(token) + 1;
+ char auth_header_val[s];
+ strlcpy(auth_header_val, AUTH_HEADER_STR_PREFIX, s);
+ strlcat(auth_header_val, token, s);
+ headers = curl_slist_append(headers, auth_header_val);
+ }
+ if (post_data) {
+ char *content_type_header_val = "Content-Type: application/json";
+ headers = curl_slist_append(headers, content_type_header_val);
+ }
+
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_response);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &write_result);
+ if (post_data)
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
+
+ status = curl_easy_perform(curl);
+ if (status) {
+ gs_log(GS_WARNING, "post_request",
+ "unable to request data from %s: %s",
+ url, curl_easy_strerror(status));
+ goto error;
+ }
+
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
+ if (code != 200) {
+ gs_log(GS_WARNING, "post_request", "server responded with code %ld",
+ code);
+ goto error;
+ }
+
+ data[write_result.pos] = '\0';
+
+ curl_slist_free_all(headers);
+ curl_easy_cleanup(curl);
+ return data;
+
+error:
+ if (data)
+ free(data);
+ if (headers)
+ curl_slist_free_all(headers);
+ if (curl)
+ curl_easy_cleanup(curl);
+ return NULL;
+}
diff --git a/libgs/src/http.h b/libgs/src/http.h
new file mode 100644
index 0000000..a0d850d
--- /dev/null
+++ b/libgs/src/http.h
@@ -0,0 +1,9 @@
+#ifndef __GS_HTTP_H
+#define __GS_HTTP_H
+
+int gs_http_init();
+void gs_http_cleanup();
+char *gs_http_get(const char *url, const char *token);
+char *gs_http_post(const char *url, const char *token, const char *data);
+
+#endif
diff --git a/libgs/src/instance_info.c b/libgs/src/instance_info.c
new file mode 100644
index 0000000..40f07c6
--- /dev/null
+++ b/libgs/src/instance_info.c
@@ -0,0 +1,38 @@
+#define _POSIX_C_SOURCE 200809L
+#include <string.h>
+#include <jansson.h>
+#include "instance_info.h"
+#include "log.h"
+
+struct instance_info *instance_info_from_json(char *json_data)
+{
+ struct instance_info *info;
+ info = malloc(sizeof(struct instance_info));
+
+ json_t *root;
+ json_error_t error;
+
+ root = json_loads(json_data, 0, &error);
+
+ if (!root) {
+ gs_log(GS_WARNING, "instance_info_from_json", "error: on line %d: %s",
+ error.line, error.text);
+ return NULL;
+ }
+
+ if (!json_is_object(root)) {
+ gs_log(GS_WARNING, "instance_info_from_json", "root is not object");
+ json_decref(root);
+ return NULL;
+ }
+
+ json_t *title = json_object_get(root, "title");
+ if (!json_is_string(title)) {
+ gs_log(GS_WARNING, "instance_info_from_json", "title is not string");
+ return NULL;
+ }
+ info->title = strdup(json_string_value(title));
+
+ json_decref(root);
+ return info;
+}
diff --git a/libgs/src/instance_info.h b/libgs/src/instance_info.h
new file mode 100644
index 0000000..cdb6e84
--- /dev/null
+++ b/libgs/src/instance_info.h
@@ -0,0 +1,31 @@
+#ifndef __GS_INSTANCE_INFO_H
+#define __GS_INSTANCE_INFO_H
+
+#include <stdbool.h>
+
+struct instance_info_urls {
+ char *streaming_api;
+};
+
+struct instance_info_stats {
+ int user_count;
+ int status_count;
+ int domain_count;
+};
+
+struct instance_info {
+ char *url;
+ char *title;
+ char *description;
+ char *email;
+ char *version;
+ struct instance_info_urls urls;
+ struct instance_info_stats states;
+ char *thumbnail;
+ char **languages;
+ bool registrations;
+};
+
+struct instance_info *instance_info_from_json(char *json_data);
+
+#endif
diff --git a/libgs/src/log.c b/libgs/src/log.c
new file mode 100644
index 0000000..dad6d66
--- /dev/null
+++ b/libgs/src/log.c
@@ -0,0 +1,30 @@
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include "log.h"
+
+static const char *log_levels[] = {
+ "info",
+ "warning",
+ "error",
+ "fatal",
+};
+
+void gs_log(enum gs_log_level level, const char *namespace,
+ const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+
+ fprintf(stderr, "%s: %s: ", log_levels[level], namespace);
+ vfprintf(stderr, format, args);
+ fprintf(stderr, "\n");
+
+ if (level == GS_ERROR) {
+ exit(EXIT_FAILURE);
+ } else if (level == GS_FATAL) {
+ abort();
+ }
+
+ va_end(args);
+}
+
diff --git a/libgs/src/log.h b/libgs/src/log.h
new file mode 100644
index 0000000..83e3784
--- /dev/null
+++ b/libgs/src/log.h
@@ -0,0 +1,13 @@
+#ifndef __GS_LOG_H
+#define __GS_LOG_H
+
+enum gs_log_level {
+ GS_INFO = 0,
+ GS_WARNING,
+ GS_ERROR,
+ GS_FATAL
+};
+
+void gs_log(enum gs_log_level level, const char *namespace, const char *format, ...);
+
+#endif
diff --git a/libgs/src/status.c b/libgs/src/status.c
new file mode 100644
index 0000000..5bcdb74
--- /dev/null
+++ b/libgs/src/status.c
@@ -0,0 +1,120 @@
+#define _POSIX_C_SOURCE 200809L
+#include <err.h>
+#include <string.h>
+#include <jansson.h>
+#include "status.h"
+#include "account.h"
+#include "log.h"
+
+struct gs_status *gs_status_from_json(const char *json_data)
+{
+ struct gs_status *s;
+ json_t *root;
+ json_error_t error;
+
+ root = json_loads(json_data, 0, &error);
+ if (!root) {
+ gs_log(GS_WARNING, "status_from_json",
+ "json parse error: line %d: %s", error.line, error.text);
+ return NULL;
+ }
+
+ s = gs_status_from_json_t(root);
+ json_decref(root);
+ return s;
+}
+
+struct gs_status *gs_status_from_json_t(const json_t *root)
+{
+ struct gs_status *s;
+ if (!root) {
+ gs_log(GS_WARNING, "gs_status_from_json_t", "json data is null");
+ return NULL;
+ }
+
+ if (!json_is_object(root)) {
+ gs_log(GS_WARNING, "gs_status_from_json_t", "json root is not object");
+ return NULL;
+ }
+
+ s = calloc(1, sizeof(struct gs_status));
+ if (!s) {
+ err(1, "gs_status_from_json_t");
+ return NULL;
+ }
+
+ json_t *id = json_object_get(root, "id");
+ if (json_is_string(id)) {
+ s->id = strdup(json_string_value(id));
+ }
+
+ json_t *uri = json_object_get(root, "uri");
+ if (json_is_string(uri)) {
+ s->uri = strdup(json_string_value(uri));
+ }
+
+ json_t *url = json_object_get(root, "url");
+ if (json_is_string(url)) {
+ s->url = strdup(json_string_value(url));
+ }
+
+ json_t *account = json_object_get(root, "account");
+ if (json_is_object(account)) {
+ s->account = gs_account_from_json_t(account);
+ }
+
+ json_t *in_reply_to_id = json_object_get(root, "in_reply_to_id");
+ if (json_is_string(in_reply_to_id)) {
+ s->in_reply_to_id = strdup(json_string_value(in_reply_to_id));
+ }
+
+ json_t *in_reply_to_account_id =
+ json_object_get(root, "in_reply_to_account_id");
+ if (json_is_string(in_reply_to_account_id)) {
+ s->in_reply_to_account_id =
+ strdup(json_string_value(in_reply_to_account_id));
+ }
+
+ json_t *content = json_object_get(root, "content");
+ if (json_is_string(content)) {
+ s->content = strdup(json_string_value(content));
+ }
+
+ json_t *replies_count = json_object_get(root, "replies_count");
+ if (json_is_integer(replies_count)) {
+ s->replies_count = json_integer_value(replies_count);
+ }
+
+ json_t *reblogs_count = json_object_get(root, "reblogs_count");
+ if (json_is_integer(reblogs_count)) {
+ s->reblogs_count = json_integer_value(reblogs_count);
+ }
+
+ json_t *favourites_count = json_object_get(root, "favourites_count");
+ if (json_is_integer(favourites_count)) {
+ s->favourites_count = json_integer_value(favourites_count);
+ }
+
+ return s;
+}
+
+void gs_status_free(struct gs_status *s)
+{
+ if (s->id)
+ free(s->id);
+ if (s->uri)
+ free(s->uri);
+ if (s->url)
+ free(s->url);
+ if (s->account)
+ gs_account_free(s->account);
+ if (s->in_reply_to_id)
+ free(s->in_reply_to_id);
+ if (s->in_reply_to_account_id)
+ free(s->in_reply_to_account_id);
+ if (s->reblog)
+ gs_status_free(s->reblog);
+ if (s->content)
+ free(s->content);
+ free(s);
+}
diff --git a/libgs/src/status.h b/libgs/src/status.h
new file mode 100644
index 0000000..431a027
--- /dev/null
+++ b/libgs/src/status.h
@@ -0,0 +1,31 @@
+#ifndef __GS_STATUS_H
+#define __GS_STATUS_H
+
+#include <stdbool.h>
+#include <jansson.h>
+
+#include "account.h"
+
+struct gs_status {
+ char *id;
+ char *uri;
+ char *url;
+ struct gs_account *account;
+ char *in_reply_to_id;
+ char *in_reply_to_account_id;
+ struct gs_status *reblog;
+ char *content;
+ unsigned int replies_count;
+ unsigned int reblogs_count;
+ unsigned int favourites_count;
+ bool reblogged;
+ bool favourited;
+ bool muted;
+ bool sensitive;
+};
+
+struct gs_status *gs_status_from_json(const char *);
+struct gs_status *gs_status_from_json_t(const json_t *);
+void gs_status_free(struct gs_status *);
+
+#endif
diff --git a/libgs/src/string-util.c b/libgs/src/string-util.c
new file mode 100644
index 0000000..7f151ae
--- /dev/null
+++ b/libgs/src/string-util.c
@@ -0,0 +1,86 @@
+/* $OpenBSD: src/lib/libc/string/strlcpy.c,v 1.11 2006/05/05 15:27:38
+ * millert Exp $ */
+
+/*
+ * Copyright (c) 1998 Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <string.h>
+#include <stdlib.h>
+
+/*
+ * Copy src to string dst of size siz. At most siz-1 characters
+ * will be copied. Always NUL terminates (unless siz == 0).
+ * Returns strlen(src); if retval >= siz, truncation occurred.
+ */
+size_t strlcpy(char *dst, const char *src, size_t siz)
+{
+ char *d = dst;
+ const char *s = src;
+ size_t n = siz;
+
+ /* Copy as many bytes as will fit */
+ if (n != 0) {
+ while (--n != 0) {
+ if ((*d++ = *s++) == '\0')
+ break;
+ }
+ }
+
+ /* Not enough room in dst, add NUL and traverse rest of src */
+ if (n == 0) {
+ if (siz != 0)
+ *d = '\0'; /* NUL-terminate dst */
+ while (*s++)
+ ;
+ }
+
+ return (s - src - 1); /* count does not include NUL */
+}
+
+/*
+ * Appends src to string dst of size siz (unlike strncat, siz is the
+ * full size of dst, not space left). At most siz-1 characters
+ * will be copied. Always NUL terminates (unless siz <= strlen(dst)).
+ * Returns strlen(src) + MIN(siz, strlen(initial dst)).
+ * If retval >= siz, truncation occurred.
+ */
+size_t strlcat(char *dst, const char *src, size_t siz)
+{
+ char *d = dst;
+ const char *s = src;
+ size_t n = siz;
+ size_t dlen;
+
+ /* Find the end of dst and adjust bytes left but don't go past end */
+ while (n-- != 0 && *d != '\0')
+ d++;
+ dlen = d - dst;
+ n = siz - dlen;
+
+ if (n == 0)
+ return (dlen + strlen(s));
+ while (*s != '\0') {
+ if (n != 1) {
+ *d++ = *s;
+ n--;
+ }
+ s++;
+ }
+ *d = '\0';
+
+ return (dlen + (s - src)); /* count does not include NUL */
+}
diff --git a/libgs/src/string-util.h b/libgs/src/string-util.h
new file mode 100644
index 0000000..6acee6d
--- /dev/null
+++ b/libgs/src/string-util.h
@@ -0,0 +1,10 @@
+#ifndef __GS_STRING_UTIL_H
+#define __GS_STRING_UTIL_H
+
+#include <sys/types.h>
+
+size_t strlcpy(char *dst, const char *src, size_t siz);
+size_t strlcat(char *dst, const char *src, size_t siz);
+char *str_replace(const char *str, const char *old, const char *new);
+
+#endif
diff --git a/libgs/src/timeline.c b/libgs/src/timeline.c
new file mode 100644
index 0000000..fa0a2e8
--- /dev/null
+++ b/libgs/src/timeline.c
@@ -0,0 +1,93 @@
+#define _POSIX_C_SOURCE 200809L
+#include <err.h>
+#include <string.h>
+#include <pthread.h>
+#include <jansson.h>
+#include "timeline.h"
+#include "auth.h"
+#include "config.h"
+#include "http.h"
+#include "string-util.h"
+#include "log.h"
+
+struct gs_timeline *gs_timeline_from_json(const char *json_data)
+{
+ struct gs_timeline *t;
+ json_t *root;
+ json_error_t error;
+
+ root = json_loads(json_data, 0, &error);
+ if (!root) {
+ gs_log(GS_WARNING, "timeline_from_json", "json root it null");
+ return NULL;
+ }
+
+ if (!json_is_array(root)) {
+ gs_log(GS_WARNING, "timeline_from_json", "json root is not array");
+ json_decref(root);
+ return NULL;
+ }
+
+ t = calloc(1, sizeof(struct gs_timeline));
+ if (!t) {
+ err(1, NULL);
+ json_decref(root);
+ return NULL;
+ }
+
+ t->size = json_array_size(root);
+ t->statuses = calloc(t->size, sizeof(struct status *));
+ if (!(t->statuses)) {
+ err(1, NULL);
+ gs_timeline_free(t);
+ json_decref(root);
+ return NULL;
+ }
+
+ json_t *data;
+ for (size_t i = 0; i < t->size; i++) {
+ data = json_array_get(root, i);
+ if (!data)
+ goto error;
+ t->statuses[i] = gs_status_from_json_t(data);
+ if (!(t->statuses[i]))
+ goto error;
+ }
+ json_decref(root);
+ return t;
+
+error:
+ gs_timeline_free(t);
+ json_decref(root);
+ return NULL;
+}
+
+void gs_timeline_free(struct gs_timeline *t)
+{
+ for (size_t i = 0; i < t->size; i++) {
+ if (t->statuses[i])
+ gs_status_free(t->statuses[i]);
+ }
+ free(t->statuses);
+ free(t);
+}
+
+struct gs_timeline *gs_timeline_get(GSClient *c, const char *max_id,
+ const char *since_id, const char *min_id, int limit)
+{
+ char *resp;
+ struct gs_timeline *t;
+
+ resp = (char *)gs_client_do_api(c, 1, "/api/v1/timelines/home", NULL);
+ if (!resp) {
+ return NULL;
+ }
+
+ t = gs_timeline_from_json(resp);
+ free(resp);
+ if (!t) {
+ return NULL;
+ }
+
+ return t;
+}
diff --git a/libgs/src/timeline.h b/libgs/src/timeline.h
new file mode 100644
index 0000000..73f46bb
--- /dev/null
+++ b/libgs/src/timeline.h
@@ -0,0 +1,17 @@
+#ifndef __GS_TIMELINE_H
+#define __GS_TIMELINE_H
+
+#include "status.h"
+#include "client.h"
+
+struct gs_timeline {
+ struct gs_status **statuses;
+ size_t size;
+};
+
+struct gs_timeline *gs_timeline_get(GSClient *c, const char *max_id,
+ const char *since_id, const char *min_id, int limit);
+struct gs_timeline *gs_timeline_from_json(const char *);
+void gs_timeline_free(struct gs_timeline *);
+
+#endif