logo Netzschleuder network catalog, repository, and centrifuge

History of truth_social

The changes to the source code of this entry in the repository, newest first, each with the differences it made to the entry's files. They are also listed on GitLab.

[entries] Add the date when each entry was added, and remove icon_hash by Tiago de Paula Peixoto beff7c9
The dates are those of the commits that first added the entries' files
(2020-08-11 for the entries of the initial commit); renamed entries keep
the date of their first name.

Changed: none
diff --git a/entries/truth_social/__init__.py b/entries/truth_social/__init__.py
index bcf6920..3657ab6 100644
--- a/entries/truth_social/__init__.py
+++ b/entries/truth_social/__init__.py
@@ -63,7 +63,7 @@ citation = [('P. Gerard, N. Botzer, T. Weninger, "Truth Social Dataset." Proceed
              'https://doi.org/10.1609/icwsm.v17i1.22211')]
 upstream_license = ('Creative Commons Attribution 4.0 International (CC BY 4.0)',
                     'https://creativecommons.org/licenses/by/4.0/')
-icon_hash = None
+date_added = '2026-10-31'
 
 # The two versions of the Zenodo record: 7531625 (v2, the latest) and 7522646
 # (v1), which differ only in truths.tsv (v1 has 20,975 more Truths)
[entries] Prefix the properties in the descriptions with (Node), (Edge) or (Graph) by Tiago de Paula Peixoto bc3dbbd
The lists of properties at the end of the descriptions marked edge
properties with "Edge" but left node properties unmarked, or grouped
them in "Nodes: `a`, ...; `b`, ..." items, so the kind of a property
was often unclear. Every item now starts with (Node), (Edge) or (Graph),
followed by the property names, a colon and the explanation, one item
per property or group of properties; qualifications (kinds of nodes,
networks) follow the names in parentheses. Properties that were only
named got a short explanation, and a few items that named the wrong
kind of property were corrected.
diff --git a/entries/truth_social/__init__.py b/entries/truth_social/__init__.py
index 593f1e7..bcf6920 100644
--- a/entries/truth_social/__init__.py
+++ b/entries/truth_social/__init__.py
@@ -38,9 +38,25 @@ The dataset was published in two versions on Zenodo, which differ only in the fi
 
 The networks have the following properties:
 
-- Nodes (users): `user_id`, the id of the user in the dataset; `has_record`, whether the user is in the file of users (a few users referenced by the follows, replies, quotes, tags and Truths are missing from it; their attributes are empty or -1); `username`; `created`, the creation month of the account (Unix time; -1 if unknown); `follower_count` and `following_count`, the numbers of followers and followed accounts shown on the profile (-1 if unknown); `time_scraped`, the time when the profile was last scraped (Unix time); `finished_follower_scrape`, `finished_following_scrape` and `finished_truth_scrape`, whether the followers, followings and Truths of the user were scraped completely at least once (1 or 0; -1 if unknown).
-- In `hashtags`, nodes also have `is_hashtag`, whether the node is a hashtag (otherwise a user), `hashtag`, the hashtag (empty for users), and `first_seen`, the time when the hashtag was first seen (Unix time; -1 for users). A few hashtags and URLs referenced by the Truths are missing from their files; their attributes are empty or -1. In `urls`, nodes also have `is_url`, whether the node is an external URL (otherwise a user), and `url`, the URL (empty for users). The user properties are empty or -1 for hashtags and URLs.
-- Edges: `time_scraped`, the time when the edge was collected (Unix time; in `follows`, `replies`, `quotes` and `mentions`); in `quotes`, `retruths`, `mentions`, `hashtags` and `urls`, `time`, the time of the (quoting, ReTruthing, tagging, linking) Truth (Unix time; -1 if unknown), and `truth_id`, the id of that Truth on Truth Social (its page is `https://truthsocial.com/@<username>/posts/<truth_id>`; -1 if the Truth is not in the file of Truths); in `quotes`, also `quoted_truth_id`, the id of the quoted Truth; in `retruths`, also `retruthed_truth_id`, the id of the ReTruthed Truth (-1 if unknown); in `retruths`, `mentions`, `hashtags` and `urls`, `like_count`, `retruth_count` and `reply_count`, the numbers of likes, ReTruths and replies of that Truth when it was scraped (for ReTruths, as recorded, usually 0)."""
+- (Node) `user_id` (users): the id of the user in the dataset.
+- (Node) `has_record` (users): whether the user is in the file of users (a few users referenced by the follows, replies, quotes, tags and Truths are missing from it; their attributes are empty or -1).
+- (Node) `username` (users): the username.
+- (Node) `created` (users): the creation month of the account (Unix time; -1 if unknown).
+- (Node) `follower_count`, `following_count` (users): the numbers of followers and followed accounts shown on the profile (-1 if unknown).
+- (Node) `time_scraped` (users): the time when the profile was last scraped (Unix time).
+- (Node) `finished_follower_scrape`, `finished_following_scrape`, `finished_truth_scrape` (users): whether the followers, followings and Truths of the user were scraped completely at least once (1 or 0; -1 if unknown).
+- (Node) `is_hashtag` (`hashtags`): whether the node is a hashtag (otherwise a user).
+- (Node) `hashtag` (`hashtags`): the hashtag (empty for users).
+- (Node) `first_seen` (`hashtags`): the time when the hashtag was first seen (Unix time; -1 for users).
+- (Node) `is_url` (`urls`): whether the node is an external URL (otherwise a user).
+- (Node) `url` (`urls`): the URL (empty for users).
+- (Node) A few hashtags and URLs referenced by the Truths are missing from their files; their attributes are empty or -1. The user properties are empty or -1 for hashtags and URLs.
+- (Edge) `time_scraped` (`follows`, `replies`, `quotes` and `mentions`): the time when the edge was collected (Unix time).
+- (Edge) `time` (`quotes`, `retruths`, `mentions`, `hashtags` and `urls`): the time of the (quoting, ReTruthing, tagging, linking) Truth (Unix time; -1 if unknown).
+- (Edge) `truth_id` (`quotes`, `retruths`, `mentions`, `hashtags` and `urls`): the id of that Truth on Truth Social (its page is `https://truthsocial.com/@<username>/posts/<truth_id>`; -1 if the Truth is not in the file of Truths).
+- (Edge) `quoted_truth_id` (`quotes`): the id of the quoted Truth.
+- (Edge) `retruthed_truth_id` (`retruths`): the id of the ReTruthed Truth (-1 if unknown).
+- (Edge) `like_count`, `retruth_count`, `reply_count` (`retruths`, `mentions`, `hashtags` and `urls`): the numbers of likes, ReTruths and replies of that Truth when it was scraped (for ReTruths, as recorded, usually 0)."""
 tags = ['Social', 'Online', 'Political', 'Bipartite', 'Unweighted', 'Multigraph', 'Timestamps', 'Metadata']
 url = 'https://doi.org/10.5281/zenodo.7531625'
 citation = [('P. Gerard, N. Botzer, T. Weninger, "Truth Social Dataset." Proceedings of the International AAAI Conference on Web and Social Media 17, 1034-1040 (2023)',
[entries] Remove unneeded code from the entries by Tiago de Paula Peixoto 2c69f19
Unneeded "global files" statements, unused imports and variables,
f-strings without placeholders, duplicate keys in foodweb_little_rock
(keeping the order and the values used), and the second definition of
get_lines() in twitter_higgs. Nothing changes in the networks or
descriptions.

Changed: none
diff --git a/entries/truth_social/__init__.py b/entries/truth_social/__init__.py
index 36d142e..593f1e7 100644
--- a/entries/truth_social/__init__.py
+++ b/entries/truth_social/__init__.py
@@ -104,7 +104,6 @@ def int_col(s):
 @coerce_props()
 @annotate()
 def parse(alts=None):
-    global files
     name = __name__.split(".")[-1]
     users = None
     truths = None
[entries] New entry: truth_social by Tiago de Paula Peixoto 8ce817f
diff --git a/entries/truth_social/__init__.py b/entries/truth_social/__init__.py
new file mode 100644
index 0000000..36d142e
--- /dev/null
+++ b/entries/truth_social/__init__.py
@@ -0,0 +1,274 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2026 Tiago de Paula Peixoto <tiago@skewed.de>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from .. import *
+import pandas
+
+title = "Truth Social users and interactions (2022)"
+description = """Networks of the users of Truth Social, the microblogging platform launched in February 2022 by the Trump Media & Technology Group, from the dataset of Gerard, Botzer and Weninger, who crawled the platform from February to October 2022 (posts of February to September), starting from popular accounts and following their followers and followings. The posts of Truth Social are called "Truths", and the reposts "ReTruths". The dataset contains 454,458 users, 4,002,115 follow relations and about 860,000 Truths with their replies, quotes, hashtags, user tags and links.
+
+The entry contains seven networks:
+
+- `follows`: a directed edge from user u to user v means that u follows v.
+- `replies`: a directed edge from a user to a user they replied to, for each reply (the replied Truths are not recorded).
+- `quotes`: a directed edge from a user to the user whose Truth they quoted, for each quote.
+- `retruths`: a directed edge from a user to the author of a Truth they ReTruthed, for each ReTruth.
+- `mentions`: a directed edge from the author of a Truth to each user tagged in it, for each tag.
+- `hashtags`: a bipartite network of users and hashtags, with an edge for each use of a hashtag in a Truth of the user.
+- `urls`: a bipartite network of users and external URLs (links to other sites), with an edge for each link in a Truth of the user.
+
+Repeated interactions appear as multiple edges, and self-interactions as self-loops. Each network contains only the users (and hashtags and URLs) with at least one edge.
+
+The dataset was published in two versions on Zenodo, which differ only in the file of Truths: the second version leaves out 20,975 Truths whose texts contain quotation marks (malformed rows of the first version), and corrects the ReTruthed Truth of one. The Truths of both versions are combined here (those of the second version, and the 20,975 Truths only in the first one), so that the networks `quotes`, `retruths`, `mentions`, `hashtags` and `urls` include all of them. Even so, some Truths referenced by the files of tags, hashtags and links, and the original Truths of some ReTruths, are not in the file of Truths, so that their authors are unknown; these edges are left out (about 26,000 hashtag uses, 24,000 user tags, 22,000 links and 7,000 ReTruths). Rows of the file of Truths that are malformed (texts containing tabs) are read from the end, by the URL of the Truth. Also left out are the texts of the Truths, and the media files (images and videos) attached to them. The users and Truths that were added by the authors to complete the references have their unknown attributes set to -1. The times are given without time zone, and are taken as UTC; the creation dates of the users are known only to the month, and those of some Truths only to the day.
+
+The networks have the following properties:
+
+- Nodes (users): `user_id`, the id of the user in the dataset; `has_record`, whether the user is in the file of users (a few users referenced by the follows, replies, quotes, tags and Truths are missing from it; their attributes are empty or -1); `username`; `created`, the creation month of the account (Unix time; -1 if unknown); `follower_count` and `following_count`, the numbers of followers and followed accounts shown on the profile (-1 if unknown); `time_scraped`, the time when the profile was last scraped (Unix time); `finished_follower_scrape`, `finished_following_scrape` and `finished_truth_scrape`, whether the followers, followings and Truths of the user were scraped completely at least once (1 or 0; -1 if unknown).
+- In `hashtags`, nodes also have `is_hashtag`, whether the node is a hashtag (otherwise a user), `hashtag`, the hashtag (empty for users), and `first_seen`, the time when the hashtag was first seen (Unix time; -1 for users). A few hashtags and URLs referenced by the Truths are missing from their files; their attributes are empty or -1. In `urls`, nodes also have `is_url`, whether the node is an external URL (otherwise a user), and `url`, the URL (empty for users). The user properties are empty or -1 for hashtags and URLs.
+- Edges: `time_scraped`, the time when the edge was collected (Unix time; in `follows`, `replies`, `quotes` and `mentions`); in `quotes`, `retruths`, `mentions`, `hashtags` and `urls`, `time`, the time of the (quoting, ReTruthing, tagging, linking) Truth (Unix time; -1 if unknown), and `truth_id`, the id of that Truth on Truth Social (its page is `https://truthsocial.com/@<username>/posts/<truth_id>`; -1 if the Truth is not in the file of Truths); in `quotes`, also `quoted_truth_id`, the id of the quoted Truth; in `retruths`, also `retruthed_truth_id`, the id of the ReTruthed Truth (-1 if unknown); in `retruths`, `mentions`, `hashtags` and `urls`, `like_count`, `retruth_count` and `reply_count`, the numbers of likes, ReTruths and replies of that Truth when it was scraped (for ReTruths, as recorded, usually 0)."""
+tags = ['Social', 'Online', 'Political', 'Bipartite', 'Unweighted', 'Multigraph', 'Timestamps', 'Metadata']
+url = 'https://doi.org/10.5281/zenodo.7531625'
+citation = [('P. Gerard, N. Botzer, T. Weninger, "Truth Social Dataset." Proceedings of the International AAAI Conference on Web and Social Media 17, 1034-1040 (2023)',
+             'https://doi.org/10.1609/icwsm.v17i1.22211')]
+upstream_license = ('Creative Commons Attribution 4.0 International (CC BY 4.0)',
+                    'https://creativecommons.org/licenses/by/4.0/')
+icon_hash = None
+
+# The two versions of the Zenodo record: 7531625 (v2, the latest) and 7522646
+# (v1), which differ only in truths.tsv (v1 has 20,975 more Truths)
+upstream_prefix = 'https://zenodo.org/records'
+V2 = '7531625/files/truth_social.zip'
+V1 = '7522646/files/truth_social.zip'
+alts = ['follows', 'replies', 'quotes', 'retruths', 'mentions', 'hashtags', 'urls']
+files = [((V2, V1) if alt not in ("follows", "replies") else V2,
+          alt, None) for alt in alts]
+
+def fetch_upstream(force=False):
+    return fetch_upstream_files(__name__.split(".")[-1], upstream_prefix, files,
+                                force)
+
+def unix(s):
+    "Unix times of the date strings in the series `s` (-1 for '-1' or empty)."
+    t = pandas.to_datetime(s.where(~s.isin(["-1", ""])), format="mixed",
+                           utc=True)
+    return numpy.where(t.isna(), -1,
+                       (t - pandas.Timestamp(0, tz="UTC")) // pandas.Timedelta(seconds=1)).astype("int64")
+
+def read_tsv(name, member, **kwargs):
+    with open_upstream_file(name, f"{V2}:truth_social/{member}.tsv", "rb") as f:
+        return pandas.read_csv(f, sep="\t", dtype=str, keep_default_na=False,
+                               quoting=3, **kwargs)
+
+TRUTH_URL = re.compile(r"^https://truthsocial\.com/@")
+
+def read_truths(name, fname):
+    """The Truths of the file `fname` as a dict mapping their ids to tuples
+    (timestamp, is_retruth, author, like_count, retruth_count, reply_count,
+    external_id, truth_retruthed). Some rows have additional fields (texts
+    containing tabs, and a username after the text): the fields after the
+    text are found from the end, by the URL of the Truth."""
+    truths = {}
+    with open_upstream_file(name, f"{fname}:truth_social/truths.tsv", "r") as f:
+        next(f)
+        for line in f:
+            x = line.rstrip("\n").split("\t")
+            k = [i for i in range(10, len(x)) if TRUTH_URL.match(x[i])]
+            if len(k) != 1:
+                raise ValueError(f"malformed Truth: {line[:200]}")
+            k = k[0]
+            rt = x[k + 1] if k + 1 < len(x) else ""
+            truths[x[0]] = (x[1], x[3] == "t", x[5], x[6], x[7], x[8],
+                            x[k - 1], rt if rt != "" else "-1")
+    return truths
+
+def int_col(s):
+    return pandas.to_numeric(s.replace({"": "-1", "t": "1", "f": "0"}),
+                             errors="coerce").fillna(-1).astype("int64").values
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+    global files
+    name = __name__.split(".")[-1]
+    users = None
+    truths = None
+    for fnames, alt, fmt in files:
+        if alts is not None and alt not in alts:
+            continue
+        if users is None:
+            users = read_tsv(name, "users")
+            uidx = {u: i for i, u in enumerate(users.id)}
+            missing = set()
+        if truths is None and alt != "follows" and alt != "replies":
+            # the Truths of v2, completed by those only in v1
+            truths = read_truths(name, V1)
+            truths.update(read_truths(name, V2))
+            truths = pandas.DataFrame.from_dict(
+                truths, orient="index",
+                columns=["timestamp", "is_retruth", "author", "like_count",
+                         "retruth_count", "reply_count", "external_id",
+                         "truth_retruthed"])
+            truths["time"] = unix(truths.timestamp)
+
+        eprops = []    # (name, type, values)
+        others = None  # (kind, keys, extra vprops) of the second side
+        if alt == "follows":
+            df = read_tsv(name, "follows")
+            src, tgt = df.follower, df.followed
+            eprops.append(("time_scraped", "int64_t", unix(df.time_scraped)))
+        elif alt == "replies":
+            df = read_tsv(name, "replies")
+            src, tgt = df.replying_user, df.replied_user
+            eprops.append(("time_scraped", "int64_t", unix(df.time_scraped)))
+        elif alt == "quotes":
+            df = read_tsv(name, "quotes")
+            src, tgt = df.quoting_user, df.quoted_user
+            eprops.append(("time_scraped", "int64_t", unix(df.time_scraped)))
+            eprops.append(("time", "int64_t", unix(df.timestamp)))
+            # the quoting Truth's id on Truth Social, if the Truth is known
+            ext = truths.external_id.to_dict()
+            eprops.append(("truth_id", "int64_t",
+                           int_col(df.quoting_truth.map(ext).fillna("-1"))))
+            eprops.append(("quoted_truth_id", "int64_t",
+                           int_col(df.quoted_truth_external_id)))
+        else:
+            if alt == "retruths":
+                t = truths[truths.is_retruth]
+                t = t[t.truth_retruthed.isin(truths.index)]
+                src = t.author
+                tgt = truths.author.reindex(t.truth_retruthed).values
+                eprops.append(("retruthed_truth_id", "int64_t",
+                               int_col(truths.external_id.reindex(t.truth_retruthed).reset_index(drop=True))))
+            else:
+                member, col = {"mentions": ("truth_user_tag_edges", "user_id"),
+                               "hashtags": ("truth_hashtag_edges", "hashtag_id"),
+                               "urls": ("truth_external_url_edges", "url_id")}[alt]
+                df = read_tsv(name, member)
+                df = df[df.truth_id.isin(truths.index)]
+                t = truths.reindex(df.truth_id)
+                src = t.author
+                tgt = df[col].values
+                if alt == "mentions":
+                    eprops.append(("time_scraped", "int64_t", unix(df.time_scraped)))
+            eprops.append(("time", "int64_t", t.time.values))
+            eprops.append(("truth_id", "int64_t", int_col(t.external_id)))
+            for c in ["like_count", "retruth_count", "reply_count"]:
+                eprops.append((c, "int64_t", int_col(t[c])))
+            if alt == "hashtags":
+                h = read_tsv(name, "hashtags")
+                others = ("hashtag", h.id.values,
+                          [("hashtag", "string", h.hashtag.values),
+                           ("first_seen", "int64_t", unix(h.first_seen))])
+            elif alt == "urls":
+                h = read_tsv(name, "external_urls")
+                h = h.iloc[:, :2]
+                h.columns = ["id", "url"]
+                others = ("url", h.id.values, [("url", "string", h.url.values)])
+
+        # users referenced but missing from the users file (a few) are
+        # added without attributes
+        for u in list(src) + ([] if others is not None else list(tgt)):
+            if u not in uidx:
+                uidx[u] = len(users)
+                users.loc[len(users)] = [u] + ["-1"] * (len(users.columns) - 1)
+                users.loc[len(users) - 1, "username"] = ""
+                users.loc[len(users) - 1, "time_scraped"] = "-1"
+                missing.add(u)
+        s = numpy.array([uidx[u] for u in src], dtype="int64")
+        if others is None:
+            tt = numpy.array([uidx[u] for u in tgt], dtype="int64")
+            present = numpy.zeros(len(users), dtype="bool")
+            present[s] = True
+            present[tt] = True
+            vids = numpy.flatnonzero(present)
+            nmap = numpy.cumsum(present) - 1
+            s, tt = nmap[s], nmap[tt]
+            g = Graph(directed=True)
+        else:
+            kind, keys, ovprops = others
+            oidx = {k: i for i, k in enumerate(keys)}
+            # hashtags and URLs missing from their files (a few) are added
+            # without attributes
+            omissing = sorted(set(tgt) - set(oidx))
+            for k in omissing:
+                oidx[k] = len(oidx)
+            keys = numpy.concatenate((keys, numpy.array(omissing, dtype=keys.dtype)))
+            ovprops = [(pn, vt, numpy.concatenate((vals, numpy.array(["" if vt == "string" else -1] * len(omissing), dtype=vals.dtype))))
+                       for pn, vt, vals in ovprops]
+            others = (kind, keys, ovprops)
+            to = numpy.array([oidx[u] for u in tgt], dtype="int64")
+            present = numpy.zeros(len(users), dtype="bool")
+            present[s] = True
+            vids = numpy.flatnonzero(present)
+            nmap = numpy.cumsum(present) - 1
+            s = nmap[s]
+            opresent = numpy.zeros(len(keys), dtype="bool")
+            opresent[to] = True
+            oids = numpy.flatnonzero(opresent)
+            tt = (numpy.cumsum(opresent) - 1)[to] + len(vids)
+            g = Graph(directed=False)
+        nu = len(vids)
+        g.add_vertex(nu + (0 if others is None else len(oids)))
+
+        u = users.iloc[vids]
+        def set_vp(pname, vtype, vals, default):
+            p = g.new_vp(vtype)
+            vals = list(vals)
+            if others is not None:
+                vals += [default] * len(oids)
+            if vtype == "string":
+                for v, x in zip(g.vertices(), vals):
+                    p[v] = x
+            else:
+                p.a = numpy.asarray(vals)
+            g.vp[pname] = p
+        set_vp("user_id", "int64_t", int_col(u.id), -1)
+        set_vp("has_record", "bool", ~u.id.isin(missing).values, False)
+        set_vp("username", "string", u.username.values, "")
+        set_vp("created", "int64_t", unix(u.timestamp), -1)
+        set_vp("follower_count", "int64_t", int_col(u.follower_count), -1)
+        set_vp("following_count", "int64_t", int_col(u.following_count), -1)
+        set_vp("time_scraped", "int64_t", unix(u.time_scraped), -1)
+        for c in ["finished_follower_scrape", "finished_following_scrape",
+                  "finished_truth_scrape"]:
+            set_vp(c, "int16_t", int_col(u[c]), -1)
+        if others is not None:
+            kind, keys, ovprops = others
+            p = g.new_vp("bool")
+            p.a[nu:] = True
+            g.vp[f"is_{kind}"] = p
+            for pname, vtype, vals in ovprops:
+                p = g.new_vp(vtype)
+                vals = vals[oids]
+                if vtype == "string":
+                    for i, x in enumerate(vals):
+                        p[nu + i] = x
+                else:
+                    p.a[:nu] = -1
+                    p.a[nu:] = vals
+                g.vp[pname] = p
+
+        eps = []
+        cols = [s, tt]
+        for pname, etype, vals in eprops:
+            g.ep[pname] = g.new_ep(etype)
+            eps.append(g.ep[pname])
+            cols.append(numpy.asarray(vals, dtype="int64"))
+        g.add_edge_list(numpy.column_stack(cols), eprops=eps)
+        yield alt, g